forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
11 KiB
230 lines
11 KiB
package com.mydreamjob.auth;
|
|
|
|
import ch.qos.logback.classic.Level;
|
|
import ch.qos.logback.classic.Logger;
|
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
import ch.qos.logback.core.read.ListAppender;
|
|
import com.mydreamjob.auth.sms.MockSmsCodeSender;
|
|
import com.mydreamjob.auth.sms.SmsCodeSender;
|
|
import com.mydreamjob.auth.sms.SmsSenderConfiguration;
|
|
import com.mydreamjob.auth.sms.UnavailableSmsCodeSender;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
|
/**
|
|
* S01 定向整改 F3:{@code app.sms.mock.enabled} 在
|
|
* true / false / 缺失 / 空值 / 仅空白 / 大写 / 非法值下都必须装配
|
|
* <strong>恰好一个</strong> {@link SmsCodeSender},且只有显式 true 才启用 Mock。
|
|
*
|
|
* <p>全部用例使用真实 {@link ApplicationContextRunner} 装配(不直接 new 配置类、
|
|
* 不测试字符串解析辅助函数、不使用 Mockito 验证)。</p>
|
|
*
|
|
* <p>{@link SenderConsumerProbe} 是<strong>消费者</strong>探针:它以构造参数方式依赖
|
|
* {@link SmsCodeSender},用于复现“构造注入时缺少 Bean”的原始缺陷场景;
|
|
* 它<strong>不生产</strong> any SmsCodeSender,因此不会掩盖生产 Bean 缺失。</p>
|
|
*/
|
|
class SmsSenderConfigurationTest {
|
|
|
|
/** 仅消费、不生产 SmsCodeSender 的探针。 */
|
|
static class SenderConsumerProbe {
|
|
final SmsCodeSender sender;
|
|
|
|
SenderConsumerProbe(SmsCodeSender sender) {
|
|
this.sender = sender;
|
|
}
|
|
}
|
|
|
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
|
.withUserConfiguration(SmsSenderConfiguration.class, SenderConsumerProbe.class);
|
|
|
|
private static final String ACC_PHONE = "19900009999";
|
|
private static final String ACC_CODE = "739251";
|
|
|
|
// ── 7 类取值行为 ────────────────────────────────────────────
|
|
|
|
@Test
|
|
void trueUsesMockSender() {
|
|
runner.withPropertyValues("app.sms.mock.enabled=true").run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(MockSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(UnavailableSmsCodeSender.class);
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void falseUsesUnavailableSender() {
|
|
runner.withPropertyValues("app.sms.mock.enabled=false").run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(UnavailableSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(MockSmsCodeSender.class);
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void missingPropertyUsesUnavailableSender() {
|
|
runner.run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(UnavailableSmsCodeSender.class);
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void blankPropertyUsesUnavailableSender() {
|
|
// F3 核心回归:空值曾导致 0 个 Bean 与模糊 Spring 装配启动失败
|
|
runner.withPropertyValues("app.sms.mock.enabled=").run(context -> {
|
|
assertThat(context)
|
|
.as("空值必须仍能启动且装配一个可用实现")
|
|
.hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(UnavailableSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(MockSmsCodeSender.class);
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void whitespacePropertyUsesUnavailableSender() {
|
|
runner.withPropertyValues("app.sms.mock.enabled= ").run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(UnavailableSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(MockSmsCodeSender.class);
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void invalidPropertyNeverEnablesMockSender() {
|
|
for (String invalid : new String[]{"yes", "1", "on", "enabled", "TRUE1", "mock", "null"}) {
|
|
runner.withPropertyValues("app.sms.mock.enabled=" + invalid).run(context -> {
|
|
assertThat(context).as("非法值 %s 不应导致启动失败", invalid).hasNotFailed();
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class))
|
|
.as("非法值 %s 绝不允许启用 Mock", invalid)
|
|
.isInstanceOf(UnavailableSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(MockSmsCodeSender.class);
|
|
});
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void uppercaseTrueBehaviorIsExplicit() {
|
|
// 明确语义:大小写不敏感 + 忽略两侧空白 → TRUE / True / " true " 视为启用 Mock
|
|
for (String accepted : new String[]{"TRUE", "True", " true ", "tRuE"}) {
|
|
runner.withPropertyValues("app.sms.mock.enabled=" + accepted).run(context -> {
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class))
|
|
.as("取值 [%s] 应视为显式启用 Mock", accepted)
|
|
.isInstanceOf(MockSmsCodeSender.class);
|
|
});
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void everySupportedConfigurationCreatesExactlyOneSenderBean() {
|
|
String[] values = {"true", "false", "", " ", "TRUE", "yes", "1", "on", "null"};
|
|
for (String value : values) {
|
|
runner.withPropertyValues("app.sms.mock.enabled=" + value).run(context -> {
|
|
assertThat(context).as("取值 [%s]", value).hasNotFailed();
|
|
assertThat(context.getBeanNamesForType(SmsCodeSender.class))
|
|
.as("取值 [%s] 必须恰好一个 SmsCodeSender Bean", value)
|
|
.hasSize(1);
|
|
});
|
|
}
|
|
// 缺失属性
|
|
runner.run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
assertThat(context.getBeanNamesForType(SmsCodeSender.class))
|
|
.as("缺失属性时必须恰好一个 SmsCodeSender Bean")
|
|
.hasSize(1);
|
|
});
|
|
}
|
|
|
|
// ── 空值场景的完整证据链(消费者注入 + 安全失败 + 无敏感日志)────
|
|
|
|
@Test
|
|
void blankPropertyStillAllowsConstructorInjectionIntoConsumer() {
|
|
runner.withPropertyValues("app.sms.mock.enabled=").run(context -> {
|
|
assertThat(context).hasNotFailed();
|
|
// 复现原始缺陷场景:构造参数注入必须成功(曾因 0 个 Bean 失败)
|
|
SenderConsumerProbe probe = context.getBean(SenderConsumerProbe.class);
|
|
assertThat(probe.sender).isNotNull().isInstanceOf(UnavailableSmsCodeSender.class);
|
|
// 同一实例,证明容器内只有一个 Bean
|
|
assertThat(probe.sender).isSameAs(context.getBean(SmsCodeSender.class));
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void blankPropertySenderFailsSafelyWithoutLeakingOrLogging() {
|
|
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
appender.start();
|
|
Logger logger = (Logger) LoggerFactory.getLogger("com.mydreamjob.auth.sms");
|
|
logger.addAppender(appender);
|
|
logger.setLevel(Level.TRACE);
|
|
try {
|
|
runner.withPropertyValues("app.sms.mock.enabled=").run(context -> {
|
|
SmsCodeSender sender = context.getBean(SmsCodeSender.class);
|
|
|
|
assertThatThrownBy(() -> sender.sendVerificationCode(ACC_PHONE, "login", ACC_CODE))
|
|
.isInstanceOf(BusinessException.class)
|
|
.satisfies(e -> {
|
|
BusinessException be = (BusinessException) e;
|
|
assertThat(be.getErrorCodeInt())
|
|
.isEqualTo(ErrorCode.SERVICE_UNAVAILABLE.getCode());
|
|
assertThat(be.getMessage())
|
|
.as("固定安全文本契约")
|
|
.isEqualTo("sms sender unavailable")
|
|
.doesNotContain(ACC_PHONE)
|
|
.doesNotContain(ACC_CODE)
|
|
.doesNotContainIgnoringCase("mock")
|
|
.doesNotContain("app.sms");
|
|
});
|
|
|
|
String logs = appender.list.stream()
|
|
.map(ILoggingEvent::getFormattedMessage)
|
|
.reduce("", (a, b) -> a + '\n' + b);
|
|
assertThat(logs)
|
|
.doesNotContain(ACC_CODE)
|
|
.doesNotContain(ACC_PHONE)
|
|
.doesNotContain("app.sms.mock.enabled");
|
|
});
|
|
} finally {
|
|
logger.detachAppender(appender);
|
|
appender.stop();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void unavailableSenderDoesNotReturnFalseSuccess() {
|
|
runner.withPropertyValues("app.sms.mock.enabled=false").run(context -> {
|
|
SmsCodeSender sender = context.getBean(SmsCodeSender.class);
|
|
|
|
assertThatThrownBy(() -> sender.sendVerificationCode(ACC_PHONE, "login", ACC_CODE))
|
|
.isInstanceOf(BusinessException.class)
|
|
.satisfies(e -> {
|
|
BusinessException be = (BusinessException) e;
|
|
assertThat(be.getErrorCodeInt()).isEqualTo(ErrorCode.SERVICE_UNAVAILABLE.getCode());
|
|
assertThat(be.getMessage())
|
|
.doesNotContain(ACC_PHONE)
|
|
.doesNotContain(ACC_CODE)
|
|
.doesNotContainIgnoringCase("mock");
|
|
});
|
|
});
|
|
}
|
|
|
|
@Test
|
|
void mockSenderLoadsOnlyWhenExplicitlyEnabled() {
|
|
runner.withPropertyValues("app.sms.mock.enabled=true").run(context -> {
|
|
assertThat(context).hasSingleBean(SmsCodeSender.class);
|
|
assertThat(context.getBean(SmsCodeSender.class)).isInstanceOf(MockSmsCodeSender.class);
|
|
assertThat(context).doesNotHaveBean(UnavailableSmsCodeSender.class);
|
|
});
|
|
}
|
|
}
|
|
|