fix(platform): sanitize Spring mail test failures

This commit is contained in:
sechmachine
2026-08-15 03:24:59 +07:00
parent 62a21132e2
commit bf6f9af78b
3 changed files with 83 additions and 5 deletions
@@ -0,0 +1,76 @@
# Test Evidence: Sanitized production mail exception feedback
- **Test type:** Web
- **Requirement IDs:** `INT-005`, `INT-008`
- **Scenario IDs:** `AC-INT-002` (production mail-failure boundary)
- **Test class/method:** `com.lab.labtimesheet.feature.integration.controller.SmtpOnboardingWebIntegrationTest#failedSmtpTestRendersActionableFeedbackWithoutActivatingTheDraft`
- **Implementation commit:** `pending`
## Protected behavior
Spring Mail delivery failures from the production SMTP adapter return the fixed Admin guidance instead of escaping
the MVC request or exposing provider diagnostics. A failed probe does not mark the draft tested or enable activation.
## Test method
MockMvc saves a valid draft, then the test SMTP boundary throws Spring's production-shaped `MailSendException` with a
distinctive deterministic diagnostic. The authenticated CSRF-protected request crosses the real controller and SMTP
configuration service, and the rendered Thymeleaf response is inspected for the fixed message, raw-text absence, and
absence of the activation action.
## Hand-derived expected result
The response is HTTP 200 on `smtp/form`, contains the fixed operator guidance, omits the exception diagnostic, and
does not offer Activate SMTP because `markTested` was never reached.
## RED
**Command**
```text
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:$PATH"
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw -Dtest=SmtpOnboardingWebIntegrationTest#failedSmtpTestRendersActionableFeedbackWithoutActivatingTheDraft test
```
**Observed result**
```text
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
MailSendException escaped as ServletException with the distinctive diagnostic instead of rendering smtp/form.
BUILD FAILURE
PostgreSQL: 18.4
```
## GREEN
**Command**
```text
./mvnw -Dtest=SmtpOnboardingWebIntegrationTest#failedSmtpTestRendersActionableFeedbackWithoutActivatingTheDraft test
```
**Observed result**
```text
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
PostgreSQL: 18.4
```
## Affected suite
**Command and result**
```text
./mvnw -Dtest=TimeConfigurationTest,BootstrapIntegrationTest,SmtpOnboardingWebIntegrationTest,AccountActivationIntegrationTest,AccountWebIntegrationTest,BootstrapOnboardingWebIntegrationTest,JavaMailSmtpProbeTest,SecurityResponseIntegrationTest test
Tests run: 22, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
PostgreSQL: 18.4
```
## External-test boundaries
The test exercises the production Spring Mail exception type without contacting an external SMTP server. It does not
prove live Mailpit/provider interoperability and contains no real credential or activation token.
@@ -9,6 +9,7 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpForm;
import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService; import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.mail.MailException;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
@@ -76,7 +77,7 @@ class SmtpController {
try { try {
smtp.testDraft(action.getDraftId(), adminId(principal), principal.getName()); smtp.testDraft(action.getDraftId(), adminId(principal), principal.getName());
return "redirect:/admin/smtp?tested"; return "redirect:/admin/smtp?tested";
} catch (IllegalArgumentException | IllegalStateException failure) { } catch (IllegalArgumentException | IllegalStateException | MailException failure) {
bindingResult.reject("smtp.test.failed", TEST_FAILURE_MESSAGE); bindingResult.reject("smtp.test.failed", TEST_FAILURE_MESSAGE);
return renderActionError(model, bindingResult); return renderActionError(model, bindingResult);
} }
@@ -26,6 +26,7 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.mail.MailSendException;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -158,7 +159,7 @@ class SmtpOnboardingWebIntegrationTest {
.andReturn().getResponse().getContentAsString(); .andReturn().getResponse().getContentAsString();
String draftId = html.replaceAll("(?s).*name=\"draftId\" value=\"([0-9]+)\".*", "$1"); String draftId = html.replaceAll("(?s).*name=\"draftId\" value=\"([0-9]+)\".*", "$1");
String rawDiagnostic = "AUTH rejected for smtp-secret-raw-diagnostic"; String rawDiagnostic = "AUTH rejected for smtp-secret-raw-diagnostic";
probe.failureMessage = rawDiagnostic; probe.failure = new MailSendException(rawDiagnostic);
mockMvc.perform(post("/admin/smtp/test") mockMvc.perform(post("/admin/smtp/test")
.with(user("admin@example.com").roles("ADMIN")) .with(user("admin@example.com").roles("ADMIN"))
@@ -185,12 +186,12 @@ class SmtpOnboardingWebIntegrationTest {
static final class RecordingProbe implements SmtpProbe { static final class RecordingProbe implements SmtpProbe {
private final List<String> recipients = new ArrayList<>(); private final List<String> recipients = new ArrayList<>();
private String failureMessage; private MailSendException failure;
@Override @Override
public void send(SmtpConnection connection, String recipient, String subject, String body) { public void send(SmtpConnection connection, String recipient, String subject, String body) {
if (failureMessage != null) { if (failure != null) {
throw new IllegalStateException(failureMessage); throw failure;
} }
recipients.add(recipient); recipients.add(recipient);
} }