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.
 
 
 

119 lines
5.7 KiB

package com.mydreamjob.auth.company;
import com.mydreamjob.audit.AuditContext;
import com.mydreamjob.auth.*;
import com.mydreamjob.company.CompanyRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@Testcontainers(disabledWithoutDocker = false)
@SpringBootTest
@ActiveProfiles("mysql-it")
class CompanyRegistrationConcurrencyMySqlTest {
private static final String PHONE = "19900000321";
private static final String CODE = "123456";
@Container
static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.4")
.withDatabaseName("company_registration_concurrency")
.withUsername("company_registration_test")
.withPassword("company-registration-test");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", MYSQL::getJdbcUrl);
registry.add("spring.datasource.username", MYSQL::getUsername);
registry.add("spring.datasource.password", MYSQL::getPassword);
registry.add("app.company-registration.enabled", () -> "true");
}
@Autowired CompanyRegistrationService service;
@Autowired SmsVerificationCodeRepository codes;
@Autowired UserRepository users;
@Autowired CompanyRepository companies;
@Autowired JdbcTemplate jdbc;
@MockBean RedisTemplate<String, Object> redis;
@AfterEach
void cleanup() {
jdbc.update("DELETE FROM auth_tokens WHERE user_id IN (SELECT id FROM users WHERE phone = ?)", PHONE);
jdbc.update("DELETE FROM companies WHERE owner_user_id IN (SELECT id FROM users WHERE phone = ?)", PHONE);
jdbc.update("DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE phone = ?)", PHONE);
jdbc.update("DELETE FROM users WHERE phone = ?", PHONE);
jdbc.update("DELETE FROM sms_verification_codes WHERE phone = ?", PHONE);
}
@Test
void oneCodeCanCreateOnlyOneCompanyIdentityWithoutTimingSleeps() throws Exception {
when(redis.hasKey(anyString())).thenReturn(false);
codes.saveAndFlush(SmsVerificationCode.builder().phone(PHONE).purpose("company-register")
.code(CODE).attempts(0).maxAttempts(5).used(false)
.createdAt(LocalDateTime.now()).expiredAt(LocalDateTime.now().plusMinutes(5)).build());
ExecutorService pool = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
List<Future<Object>> futures = new ArrayList<>();
for (int i = 0; i < 2; i++) {
futures.add(pool.submit(() -> {
ready.countDown();
assertThat(start.await(10, TimeUnit.SECONDS)).isTrue();
try {
return service.register(request(), AuditContext.builder().build());
} catch (Throwable error) {
return error;
}
}));
}
assertThat(ready.await(10, TimeUnit.SECONDS)).isTrue();
start.countDown();
Object first = futures.get(0).get(30, TimeUnit.SECONDS);
Object second = futures.get(1).get(30, TimeUnit.SECONDS);
pool.shutdownNow();
long successes = List.of(first, second).stream().filter(CompanyRegistrationResponse.class::isInstance).count();
assertThat(successes).isEqualTo(1);
assertThat(users.findAll().stream().filter(u -> PHONE.equals(u.getPhone())).count()).isEqualTo(1);
Long userId = users.findByPhone(PHONE).orElseThrow().getId();
assertThat(jdbc.queryForList("SELECT role FROM user_roles WHERE user_id = ?", String.class, userId))
.containsExactly("COMPANY");
assertThat(companies.findByOwnerUserId(userId)).isPresent();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM auth_tokens WHERE user_id = ?", Long.class, userId))
.isEqualTo(1L);
assertThat(List.of(first, second).stream().filter(Throwable.class::isInstance)
.map(Object::toString).allMatch(text -> !text.contains("SQL") && !text.contains("phone"))).isTrue();
}
private CompanyRegistrationRequest request() {
CompanyRegistrationRequest request = new CompanyRegistrationRequest();
request.setPhone(PHONE); request.setCode(CODE); request.setPassword("Company-strong-password-2026");
CompanyRegistrationRequest.CompanyDetails details = new CompanyRegistrationRequest.CompanyDetails();
details.setName("并发企业"); details.setIndustry("互联网"); details.setDescription("简介"); details.setAddress("地址");
request.setCompany(details);
return request;
}
}