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
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());
|
|
}
|
|
}
|
|
|