package com.mydreamjob.auth.company; import com.mydreamjob.audit.AuditContext; import com.mydreamjob.audit.AuditService; import com.mydreamjob.auth.AuthSessionService; import com.mydreamjob.auth.SmsVerificationService; import com.mydreamjob.auth.User; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.auth.password.PasswordPolicy; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyRepository; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.deletion.AccountDeletionPhoneHasher; import com.mydreamjob.deletion.AccountDeletionRepository; 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.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.dao.DataAccessResourceFailureException; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CompanyRegistrationServiceTest { private static final String PHONE = "19900000111"; @Mock SmsVerificationService sms; @Mock UserRepository users; @Mock CompanyRepository companies; @Mock AccountDeletionRepository deletions; @Mock AccountDeletionPhoneHasher hasher; @Mock AuthSessionService sessions; @Mock AuditService audit; private final PasswordEncoder encoder = new BCryptPasswordEncoder(); private final PasswordPolicy policy = new PasswordPolicy(); private CompanyRegistrationProperties properties; private CompanyRegistrationService service; @BeforeEach void setUp() { properties = new CompanyRegistrationProperties(); properties.setEnabled("true"); service = new CompanyRegistrationService(properties, sms, users, companies, policy, encoder, deletions, hasher, sessions, audit, Clock.fixed(Instant.parse("2026-09-22T00:00:00Z"), ZoneOffset.UTC)); } @Test void createsCompanyUserCompanyAndSession() { when(hasher.hash(PHONE)).thenReturn("phone-hash"); when(deletions.findByDeletedPhoneHash("phone-hash")).thenReturn(Optional.empty()); when(users.findByPhoneForUpdate(PHONE)).thenReturn(Optional.empty()); when(users.saveAndFlush(any(User.class))).thenAnswer(inv -> { User u = inv.getArgument(0); u.setId(11L); return u; }); when(companies.saveAndFlush(any(Company.class))).thenAnswer(inv -> { Company c = inv.getArgument(0); c.setId(21L); return c; }); when(sessions.createSession(any(), any(), any())) .thenReturn(new AuthSessionService.SessionTokens("access", "refresh", "sid")); CompanyRegistrationResponse result = service.register(request(), AuditContext.builder().build()); assertThat(result.role()).isEqualTo("COMPANY"); assertThat(result.onboardingStatus()).isEqualTo("UNVERIFIED"); verify(sms).verifyCode(PHONE, "123456", "company-register"); verify(users).saveAndFlush(argThat(u -> u.getRole() == User.Role.COMPANY && u.getRoles().contains(User.Role.COMPANY) && u.getRoles().size() == 1 && !u.getVerified() && u.getPasswordHash() != null)); verify(companies).saveAndFlush(argThat(c -> c.getOwnerUserId().equals(11L) && !c.getVerified())); verify(audit).logRequired(eq(11L), eq("COMPANY_REGISTERED"), eq("COMPANY"), eq(21L), anyString(), any(), any()); } @Test void disabledRegistrationFailsBeforeSmsConsumption() { properties.setEnabled("false"); assertThatThrownBy(() -> service.register(request(), AuditContext.builder().build())) .isInstanceOf(BusinessException.class) .extracting("errorCodeInt").isEqualTo(ErrorCode.COMPANY_REGISTRATION_DISABLED.getCode()); verifyNoInteractions(sms, users, companies); } @Test void dependencyFailureIsMappedToStableServiceUnavailableCode() { when(sms.verifyCode(anyString(), anyString(), eq("company-register"))) .thenThrow(new DataAccessResourceFailureException("connection details must not escape")); assertThatThrownBy(() -> service.register(request(), AuditContext.builder().build())) .isInstanceOf(BusinessException.class) .extracting("errorCodeInt") .isEqualTo(ErrorCode.COMPANY_REGISTRATION_DEPENDENCY_UNAVAILABLE.getCode()); verifyNoInteractions(users, companies, sessions, audit); } @Test void allExistingPrincipalRolesUseTheSameConflictResponse() { when(hasher.hash(PHONE)).thenReturn("phone-hash"); when(deletions.findByDeletedPhoneHash("phone-hash")).thenReturn(Optional.empty()); for (User.Role role : User.Role.values()) { reset(users); User existing = User.builder().id(100L).phone(PHONE).role(role).roles(java.util.Set.of(role)).build(); when(users.findByPhoneForUpdate(PHONE)).thenReturn(Optional.of(existing)); assertThatThrownBy(() -> service.register(request(), AuditContext.builder().build())) .isInstanceOf(BusinessException.class) .extracting("errorCodeInt") .isEqualTo(ErrorCode.COMPANY_REGISTRATION_CONFLICT.getCode()); } } private CompanyRegistrationRequest request() { CompanyRegistrationRequest request = new CompanyRegistrationRequest(); request.setPhone(PHONE); request.setCode("123456"); request.setPassword("StrongCompanyPassword#1"); CompanyRegistrationRequest.CompanyDetails details = new CompanyRegistrationRequest.CompanyDetails(); details.setName("示例企业"); details.setIndustry("互联网"); details.setDescription("企业简介"); details.setAddress("上海"); request.setCompany(details); return request; } }