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.
157 lines
6.4 KiB
157 lines
6.4 KiB
package com.mydreamjob.auth;
|
|
|
|
import com.mydreamjob.audit.AuditContext;
|
|
import com.mydreamjob.audit.AuditService;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.deletion.AccountDeletion;
|
|
import com.mydreamjob.deletion.AccountDeletionPhoneHasher;
|
|
import com.mydreamjob.deletion.AccountDeletionRepository;
|
|
import com.mydreamjob.deletion.DeletionStatus;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
|
import java.time.Clock;
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.*;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
/**
|
|
* S02 {@link AuthService} 单元测试。
|
|
*
|
|
* <p>登录链路改为「验证码校验 → 用户解析 → 创建服务端会话」,因此 Mockito 目标
|
|
* 由旧的 {@code JwtTokenProvider} 换成 {@link AuthSessionService};
|
|
* 原有断言语义(响应字段、自动建号、注销墓碑、验证码用途透传)保持不变。</p>
|
|
*/
|
|
@ExtendWith(MockitoExtension.class)
|
|
class AuthServiceTest {
|
|
|
|
private static final Instant FIXED_NOW = Instant.parse("2026-09-14T02:00:00Z");
|
|
|
|
@Mock
|
|
private UserRepository userRepository;
|
|
|
|
@Mock
|
|
private AuthSessionService authSessionService;
|
|
|
|
@Mock
|
|
private SmsVerificationService smsVerificationService;
|
|
|
|
@Mock
|
|
private AuditService auditService;
|
|
|
|
@Mock
|
|
private AccountDeletionRepository accountDeletionRepository;
|
|
|
|
@Mock
|
|
private AccountDeletionPhoneHasher accountDeletionPhoneHasher;
|
|
|
|
@Mock
|
|
private Clock clock;
|
|
|
|
@InjectMocks
|
|
private AuthService authService;
|
|
|
|
private void stubSessionIssuance() {
|
|
when(clock.instant()).thenReturn(FIXED_NOW);
|
|
when(authSessionService.createSession(any(), any(), any()))
|
|
.thenReturn(new AuthSessionService.SessionTokens("access-token", "refresh-token", "sid-1"));
|
|
}
|
|
|
|
@Test
|
|
void mockLogin_shouldReturnTokenWhenCodeValid() {
|
|
User user = User.builder()
|
|
.id(1L).phone("13800000001").nickname("测试用户")
|
|
.role(User.Role.STUDENT).active(true).verified(true).build();
|
|
|
|
when(smsVerificationService.verifyCode("13800000001", "123456", "login")).thenReturn(true);
|
|
when(userRepository.findByPhone("13800000001")).thenReturn(Optional.of(user));
|
|
stubSessionIssuance();
|
|
|
|
Map<String, Object> result = authService.mockLogin("13800000001", "123456", "login", "req-1", "10.0.0.1", "JUnit");
|
|
|
|
assertEquals("access-token", result.get("accessToken"));
|
|
assertEquals("refresh-token", result.get("refreshToken"));
|
|
assertEquals(1L, result.get("userId"));
|
|
// expiresIn 契约保持不变(设计 D04)
|
|
assertEquals(86400L, result.get("expiresIn"));
|
|
// 登录审计仍写入,且时间来自入口单次捕获的 capturedNow
|
|
verify(auditService, times(1)).log(eq(1L), eq("LOGIN"), eq("USER"), eq(1L),
|
|
eq("User logged in"), any(AuditContext.class),
|
|
eq(LocalDateTime.ofInstant(FIXED_NOW, AuthSessionService.BUSINESS_ZONE)));
|
|
}
|
|
|
|
@Test
|
|
void mockLogin_shouldCreateUserWhenNotFound() {
|
|
when(smsVerificationService.verifyCode("13800000099", "123456", "login")).thenReturn(true);
|
|
when(userRepository.findByPhone("13800000099")).thenReturn(Optional.empty());
|
|
when(userRepository.save(any())).thenAnswer(invocation -> {
|
|
User u = invocation.getArgument(0);
|
|
u.setId(99L);
|
|
return u;
|
|
});
|
|
stubSessionIssuance();
|
|
|
|
Map<String, Object> result = authService.mockLogin("13800000099", "123456", "login", null, null, null);
|
|
|
|
assertEquals(99L, result.get("userId"));
|
|
assertEquals(User.Role.STUDENT, result.get("role"));
|
|
}
|
|
|
|
@Test
|
|
void mockLogin_shouldPropagatePurposeToVerifyCode() {
|
|
when(smsVerificationService.verifyCode("13800000001", "123456", "password-reset"))
|
|
.thenReturn(true);
|
|
User user = User.builder().id(1L).phone("13800000001").role(User.Role.STUDENT).active(true).verified(true).build();
|
|
when(userRepository.findByPhone("13800000001")).thenReturn(Optional.of(user));
|
|
stubSessionIssuance();
|
|
|
|
authService.mockLogin("13800000001", "123456", "password-reset", null, null, null);
|
|
verify(smsVerificationService, times(1)).verifyCode("13800000001", "123456", "password-reset");
|
|
}
|
|
|
|
@Test
|
|
void mockLogin_shouldThrowWhenCodeInvalid() {
|
|
when(smsVerificationService.verifyCode(any(), any(), any()))
|
|
.thenThrow(new BusinessException(ErrorCode.SMS_CODE_INVALID));
|
|
|
|
assertThrows(BusinessException.class,
|
|
() -> authService.mockLogin("13800000001", "000000", "login", null, null, null));
|
|
// 验证码失败不得创建任何会话
|
|
verify(authSessionService, never()).createSession(any(), any(), any());
|
|
}
|
|
|
|
@Test
|
|
void mockLogin_shouldBlockReRegistrationOfDeletedPhone() {
|
|
when(smsVerificationService.verifyCode("13800000001", "123456", "login")).thenReturn(true);
|
|
when(accountDeletionPhoneHasher.hash("13800000001")).thenReturn("deadbeef");
|
|
AccountDeletion tomb = AccountDeletion.builder()
|
|
.userId(1L).status(DeletionStatus.COMPLETED).deletedPhoneHash("deadbeef").build();
|
|
when(accountDeletionRepository.findByDeletedPhoneHash("deadbeef")).thenReturn(Optional.of(tomb));
|
|
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> authService.mockLogin("13800000001", "123456", "login", null, null, null));
|
|
assertEquals(ErrorCode.ACCOUNT_DELETED.getCode(), ex.getErrorCodeInt());
|
|
// 不得自动创建新用户
|
|
verify(userRepository, never()).save(any());
|
|
verify(authSessionService, never()).createSession(any(), any(), any());
|
|
}
|
|
|
|
@Test
|
|
void getCurrentUser_shouldThrowWhenUserNotFound() {
|
|
when(userRepository.findById(999L)).thenReturn(Optional.empty());
|
|
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> authService.getCurrentUser(999L));
|
|
assertEquals(ErrorCode.USER_NOT_FOUND.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
}
|
|
|