Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");

Expand Down Expand Up @@ -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");

Expand All @@ -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;
}
}
Loading