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.
 
 
 

262 lines
10 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.config.AdminBootstrapProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* S03-A 管理员初始化的规则分支(设计 §3.2)。
*
* <p>真实 {@link AdminBootstrapRunner} + Mock 仓储,逐条覆盖任务书 §四 要求的场景。
* 日志脱敏用真实 Logback 捕获验证。</p>
*/
@ExtendWith(MockitoExtension.class)
class AdminBootstrapRunnerTest {
private static final String PHONE = "13800000009";
private static final String OTHER_PHONE = "13800000008";
@Mock private UserRepository userRepository;
/** S03 整改后创建走独立事务组件(JdbcTemplate),单测以 mock 替代。 */
@Mock private AdminBootstrapCreator creator;
private AdminBootstrapProperties properties;
private AdminBootstrapRunner runner;
private ListAppender<ILoggingEvent> appender;
private Logger appLogger;
@BeforeEach
void setUp() {
properties = new AdminBootstrapProperties();
runner = new AdminBootstrapRunner(properties, userRepository, creator);
appLogger = (Logger) LoggerFactory.getLogger("com.mydreamjob");
appender = new ListAppender<>();
appender.start();
appLogger.addAppender(appender);
appLogger.setLevel(Level.TRACE);
}
@AfterEach
void tearDown() {
if (appLogger != null && appender != null) {
appLogger.detachAppender(appender);
appender.stop();
}
}
private String capturedLog() {
return appender.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.joining("\n"));
}
private void enableWith(String phone) {
properties.setEnabled("true");
properties.setPhone(phone);
}
// ── 关闭 / 未启用 ──────────────────────────────────────────
@Test
void disabledDoesNothing() {
properties.setEnabled("false");
properties.setPhone(PHONE);
runner.run(null);
verifyNoInteractions(userRepository);
verifyNoInteractions(creator);
}
@Test
void nonLiteralTrueValueDoesNotEnable() {
// 只有字面量 "true" 才算启用;大小写变体一律不生效。
for (String value : new String[]{"TRUE", "True", "yes", "1", "on", " true "}) {
properties.setEnabled(value);
properties.setPhone(PHONE);
runner.run(null);
}
verifyNoInteractions(userRepository);
verifyNoInteractions(creator);
}
// ── 手机号校验 ─────────────────────────────────────────────
@Test
void missingPhoneFailsAtStartup() {
properties.setEnabled("true");
properties.setPhone("");
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null));
assertEquals(AdminBootstrapRunner.ERROR_INVALID_PHONE, ex.getMessage());
verifyNoInteractions(userRepository);
verifyNoInteractions(creator);
}
@Test
void invalidPhoneFailsAtStartup() {
for (String bad : new String[]{"123", "1380000000", "138000000012", "1380000000a", " 1380000000a "}) {
properties.setEnabled("true");
properties.setPhone(bad);
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null));
assertEquals(AdminBootstrapRunner.ERROR_INVALID_PHONE, ex.getMessage());
}
verifyNoInteractions(userRepository);
verifyNoInteractions(creator);
}
// ── 已有活动 ADMIN ─────────────────────────────────────────
@Test
void existingActiveAdminIsSkippedWithoutSideEffects() {
enableWith(PHONE);
when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(1L);
runner.run(null);
verify(creator, never()).createAdmin(any(), any());
assertTrue(capturedLog().contains("admin bootstrap skipped"), "应输出固定跳过文本");
}
// ── 手机号已被占用 ─────────────────────────────────────────
@Test
void phoneBelongingToStudentFails() {
enableWith(PHONE);
when(creator.findByPhone(PHONE)).thenReturn(Optional.of(
new AdminBootstrapCreator.BootstrapUser(9L, "STUDENT", true, true, true, List.of("STUDENT"))));
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null));
assertEquals(AdminBootstrapRunner.ERROR_PHONE_IN_USE, ex.getMessage());
verify(creator, never()).createAdmin(any(), any());
}
@Test
void phoneBelongingToCompanyFails() {
enableWith(PHONE);
when(creator.findByPhone(PHONE)).thenReturn(Optional.of(
new AdminBootstrapCreator.BootstrapUser(9L, "COMPANY", true, true, true, List.of("COMPANY"))));
assertThrows(IllegalStateException.class, () -> runner.run(null));
verify(creator, never()).createAdmin(any(), any());
}
@Test
void phoneBelongingToInactiveUserFailsAndIsNotRevived() {
enableWith(PHONE);
when(creator.findByPhone(PHONE)).thenReturn(Optional.of(
new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", false, true, true, List.of("ADMIN"))));
assertThrows(IllegalStateException.class, () -> runner.run(null));
verify(creator, never()).createAdmin(any(), any());
}
@Test
void activeAdminMissingAdminRoleCannotBypassStrictIdempotencyCheck() {
assertInvalidExistingAdminRejected(
new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", true, true, true, List.of()));
}
@Test
void activeAdminWithAdditionalRoleCannotBypassStrictIdempotencyCheck() {
assertInvalidExistingAdminRejected(
new AdminBootstrapCreator.BootstrapUser(
9L, "ADMIN", true, true, true, List.of("ADMIN", "STUDENT")));
}
@Test
void activeAdminWithPasswordCannotBypassStrictIdempotencyCheck() {
assertInvalidExistingAdminRejected(
new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", true, true, false, List.of("ADMIN")));
}
// ── 正常创建 ───────────────────────────────────────────────
@Test
void createsActiveVerifiedAdminWithConsistentRole() {
enableWith(PHONE);
when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L);
when(creator.findByPhone(PHONE)).thenReturn(Optional.empty());
when(creator.createAdmin(any(), any())).thenReturn(AdminBootstrapCreator.CreateResult.CREATED);
runner.run(null);
// 创建由独立事务组件完成(JdbcTemplate),单测只验证编排参数;
// 「users.role / user_roles 一致、active、verified、password_hash 为空」由
// AdminBootstrapIntegrationTest 与 AdminBootstrapConcurrencyLoggingTest 在真实库上断言。
verify(creator).createAdmin(eq(PHONE), any());
verify(userRepository, never()).save(any());
}
@Test
void customNicknameIsApplied() {
enableWith(PHONE);
properties.setNickname("运维管理员");
when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L);
when(creator.findByPhone(PHONE)).thenReturn(Optional.empty());
when(creator.createAdmin(any(), any())).thenReturn(AdminBootstrapCreator.CreateResult.CREATED);
runner.run(null);
verify(creator).createAdmin(PHONE, "运维管理员");
}
// ── 日志脱敏 ───────────────────────────────────────────────
@Test
void logNeverContainsPhone() {
enableWith(PHONE);
when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L);
when(creator.findByPhone(OTHER_PHONE)).thenReturn(Optional.empty());
when(creator.createAdmin(any(), any())).thenReturn(AdminBootstrapCreator.CreateResult.CREATED);
properties.setPhone(OTHER_PHONE);
runner.run(null);
String log = capturedLog();
assertThatNotContains(log, OTHER_PHONE);
assertThatNotContains(log, PHONE);
}
@Test
void failureMessagesNeverContainPhone() {
enableWith(PHONE);
when(creator.findByPhone(PHONE)).thenReturn(Optional.of(
new AdminBootstrapCreator.BootstrapUser(9L, "STUDENT", true, true, true, List.of("STUDENT"))));
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null));
assertThatNotContains(ex.getMessage(), PHONE);
}
private static void assertThatNotContains(String haystack, String needle) {
assertFalse(haystack != null && haystack.contains(needle),
"输出不得包含手机号");
}
private void assertInvalidExistingAdminRejected(AdminBootstrapCreator.BootstrapUser existing) {
enableWith(PHONE);
when(creator.findByPhone(PHONE)).thenReturn(Optional.of(existing));
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null));
assertEquals(AdminBootstrapRunner.ERROR_PHONE_IN_USE, ex.getMessage());
verify(creator, never()).createAdmin(any(), any());
}
}