From 3887e90464aea170a25a63993820a0f2fc31fbf2 Mon Sep 17 00:00:00 2001 From: hywznn Date: Fri, 7 Aug 2026 14:53:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(auth):=20SMTP=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=84=20=EA=B8=B0=EB=8F=99=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SMTP Provider 활성화 시 host를 필수로 확인하고 인증 사용 시 username과 password 누락을 거부합니다. 정상 설정과 빈 host·인증정보 누락 테스트를 추가했습니다. --- .../SmtpPasswordResetNotificationAdapter.java | 27 ++++++++- .../PasswordResetSmtpConfigurationTest.java | 40 ++++++++++++- ...pPasswordResetNotificationAdapterTest.java | 56 ++++++++++++++++++- 3 files changed, 118 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapter.java b/src/main/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapter.java index 4b8ac4d..dc35c2a 100644 --- a/src/main/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapter.java +++ b/src/main/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapter.java @@ -4,9 +4,11 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.mail.autoconfigure.MailProperties; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; @Component @@ -22,13 +24,36 @@ public final class SmtpPasswordResetNotificationAdapter implements PasswordReset public SmtpPasswordResetNotificationAdapter( JavaMailSender mailSender, - PasswordResetNotificationProperties properties + PasswordResetNotificationProperties properties, + MailProperties mailProperties ) { properties.validateForSmtp(); + validateMailProperties(mailProperties); this.mailSender = mailSender; this.properties = properties; } + private void validateMailProperties(MailProperties mailProperties) { + if (!StringUtils.hasText(mailProperties.getHost())) { + throw new IllegalStateException( + "SPRING_MAIL_HOST must not be blank when the SMTP notification provider is enabled" + ); + } + boolean authenticationRequired = Boolean.parseBoolean( + mailProperties.getProperties().getOrDefault("mail.smtp.auth", "false") + ); + if (authenticationRequired && !StringUtils.hasText(mailProperties.getUsername())) { + throw new IllegalStateException( + "SPRING_MAIL_USERNAME is required when SMTP authentication is enabled" + ); + } + if (authenticationRequired && !StringUtils.hasText(mailProperties.getPassword())) { + throw new IllegalStateException( + "SPRING_MAIL_PASSWORD is required when SMTP authentication is enabled" + ); + } + } + @Override public void sendResetLink(String email, String rawToken, Instant expiresAt) { SimpleMailMessage message = new SimpleMailMessage(); diff --git a/src/test/java/com/fowoco/server/auth/infrastructure/notification/PasswordResetSmtpConfigurationTest.java b/src/test/java/com/fowoco/server/auth/infrastructure/notification/PasswordResetSmtpConfigurationTest.java index 5a247ac..50714e2 100644 --- a/src/test/java/com/fowoco/server/auth/infrastructure/notification/PasswordResetSmtpConfigurationTest.java +++ b/src/test/java/com/fowoco/server/auth/infrastructure/notification/PasswordResetSmtpConfigurationTest.java @@ -1,11 +1,17 @@ package com.fowoco.server.auth.infrastructure.notification; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; import com.fowoco.server.auth.application.port.PasswordResetNotificationPort; +import java.net.URI; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.mail.autoconfigure.MailProperties; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -15,7 +21,9 @@ "app.auth.password-reset.notification.provider=smtp", "app.auth.password-reset.notification.from=no-reply@fowoco.test", "app.auth.password-reset.notification.reset-url=https://demo.fowoco.test/reset-password", - "app.auth.password-reset.notification.subject=FOWOCO 비밀번호 재설정" + "app.auth.password-reset.notification.subject=FOWOCO 비밀번호 재설정", + "spring.mail.host=smtp.fowoco.test", + "management.health.mail.enabled=false" }) class PasswordResetSmtpConfigurationTest { @@ -29,4 +37,34 @@ class PasswordResetSmtpConfigurationTest { void smtpProviderSelectsSmtpAdapterInsteadOfNoOp() { assertThat(notificationPort).isInstanceOf(SmtpPasswordResetNotificationAdapter.class); } + + @Test + void blankMailHostPreventsSmtpContextStartup() { + JavaMailSender mailSender = mock(JavaMailSender.class); + PasswordResetNotificationProperties notificationProperties = + new PasswordResetNotificationProperties( + "no-reply@fowoco.test", + URI.create("https://demo.fowoco.test/reset-password"), + "FOWOCO 비밀번호 재설정" + ); + MailProperties mailProperties = new MailProperties(); + + assertThatThrownBy(() -> { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + TestPropertyValues.of("app.auth.password-reset.notification.provider=smtp") + .applyTo(context); + context.registerBean( + SmtpPasswordResetNotificationAdapter.class, + () -> new SmtpPasswordResetNotificationAdapter( + mailSender, + notificationProperties, + mailProperties + ) + ); + context.refresh(); + context.getBean(SmtpPasswordResetNotificationAdapter.class); + } + }).hasRootCauseInstanceOf(IllegalStateException.class) + .hasStackTraceContaining("SPRING_MAIL_HOST"); + } } diff --git a/src/test/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapterTest.java b/src/test/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapterTest.java index f5c4cff..06b6342 100644 --- a/src/test/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapterTest.java +++ b/src/test/java/com/fowoco/server/auth/infrastructure/notification/SmtpPasswordResetNotificationAdapterTest.java @@ -8,6 +8,7 @@ import java.net.URI; import java.time.Instant; import org.junit.jupiter.api.Test; +import org.springframework.boot.mail.autoconfigure.MailProperties; import org.mockito.ArgumentCaptor; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @@ -23,7 +24,8 @@ void sendsResetLinkWithTokenAndExpiry() { "no-reply@fowoco.test", URI.create("https://demo.fowoco.test/reset-password"), "FOWOCO 비밀번호 재설정" - ) + ), + mailProperties("smtp.fowoco.test", false, null, null) ); Instant expiresAt = Instant.parse("2026-08-07T06:30:00Z"); @@ -51,7 +53,8 @@ void rejectsUnsafeOrIncompleteSmtpConfiguration() { "", URI.create("https://demo.fowoco.test/reset-password"), "FOWOCO 비밀번호 재설정" - ) + ), + mailProperties("smtp.fowoco.test", false, null, null) )).isInstanceOf(IllegalStateException.class) .hasMessageContaining("PASSWORD_RESET_MAIL_FROM"); @@ -61,8 +64,55 @@ void rejectsUnsafeOrIncompleteSmtpConfiguration() { "no-reply@fowoco.test", URI.create("file:///tmp/reset-password"), "FOWOCO 비밀번호 재설정" - ) + ), + mailProperties("smtp.fowoco.test", false, null, null) )).isInstanceOf(IllegalStateException.class) .hasMessageContaining("PASSWORD_RESET_CLIENT_URL"); } + + @Test + void rejectsMissingHostAndAuthenticationCredentials() { + JavaMailSender mailSender = mock(JavaMailSender.class); + PasswordResetNotificationProperties notificationProperties = + new PasswordResetNotificationProperties( + "no-reply@fowoco.test", + URI.create("https://demo.fowoco.test/reset-password"), + "FOWOCO 비밀번호 재설정" + ); + + assertThatThrownBy(() -> new SmtpPasswordResetNotificationAdapter( + mailSender, + notificationProperties, + mailProperties("", false, null, null) + )).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("SPRING_MAIL_HOST"); + + assertThatThrownBy(() -> new SmtpPasswordResetNotificationAdapter( + mailSender, + notificationProperties, + mailProperties("smtp.fowoco.test", true, "", "password") + )).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("SPRING_MAIL_USERNAME"); + + assertThatThrownBy(() -> new SmtpPasswordResetNotificationAdapter( + mailSender, + notificationProperties, + mailProperties("smtp.fowoco.test", true, "mailer", "") + )).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("SPRING_MAIL_PASSWORD"); + } + + private MailProperties mailProperties( + String host, + boolean authenticationRequired, + String username, + String password + ) { + MailProperties properties = new MailProperties(); + properties.setHost(host); + properties.setUsername(username); + properties.setPassword(password); + properties.getProperties().put("mail.smtp.auth", Boolean.toString(authenticationRequired)); + return properties; + } }