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.
 
 
 

174 lines
11 KiB

package com.mydreamjob.integration;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
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.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** 服务器真实版本的全迁移、Hibernate validate 和接口集合门禁,不能静默跳过。 */
@Testcontainers(disabledWithoutDocker = false)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("mysql-it")
class LxqModulesMySql57IntegrationTest {
@Container static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:5.7.36")
.withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci")
.withDatabaseName("module57_test")
.withUsername("module_test").withPassword("module-test-only");
@DynamicPropertySource static void properties(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", MYSQL::getJdbcUrl);
r.add("spring.datasource.username", MYSQL::getUsername);
r.add("spring.datasource.password", MYSQL::getPassword);
}
@MockBean RedisTemplate<String, Object> redis;
@Autowired JdbcTemplate jdbc;
@Autowired MockMvc mvc;
@Autowired ObjectMapper mapper;
@Autowired com.mydreamjob.messaging.MessageRepository messages;
@Autowired jakarta.persistence.EntityManagerFactory entityManagerFactory;
@Autowired com.mydreamjob.application.ApplicationService applications;
@Test void studentApplicationSummaryUsesOneBatchQueryOnMysql57() {
String key = "summary57-" + java.util.UUID.randomUUID().toString().substring(0, 8);
jdbc.update("INSERT INTO users (phone,role,is_active,is_verified) VALUES (?,'STUDENT',1,1)", key);
Long user = jdbc.queryForObject("SELECT id FROM users WHERE phone=?", Long.class, key);
Long company = null;
Long job = null;
try {
jdbc.update("INSERT INTO companies (owner_user_id,name,verified,created_at,updated_at) "
+ "VALUES (?,?,1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", user, key);
company = jdbc.queryForObject("SELECT id FROM companies WHERE owner_user_id=?", Long.class, user);
jdbc.update("INSERT INTO jobs (company_id,title,description,city,employment_type,status,salary_min,salary_max,created_at,updated_at) "
+ "VALUES (?,?,'fixture','上海','FULL_TIME','CLOSED',12000,24000,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", company,key);
job = jdbc.queryForObject("SELECT id FROM jobs WHERE company_id=?", Long.class, company);
jdbc.update("INSERT INTO applications (job_id,student_user_id,status,created_at,updated_at) "
+ "VALUES (?,?,'SUBMITTED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", job,user);
var stats = entityManagerFactory.unwrap(org.hibernate.SessionFactory.class).getStatistics();
boolean wasEnabled = stats.isStatisticsEnabled();
stats.setStatisticsEnabled(true);
try {
long before = stats.getPrepareStatementCount();
var page = applications.listStudentApplicationViews(user, null, 0, 1);
assertThat(stats.getPrepareStatementCount() - before).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(1);
assertThat(page.getContent().get(0)).containsEntry("jobTitle",key)
.containsEntry("companyName",key).containsEntry("city","上海")
.containsEntry("salaryMin",12000).containsEntry("salaryMax",24000)
.containsEntry("jobStatus","CLOSED").containsEntry("status","SUBMITTED")
.containsEntry("employmentType","FULL_TIME");
} finally { stats.setStatisticsEnabled(wasEnabled); }
} finally {
if (job != null) {
jdbc.update("DELETE FROM applications WHERE job_id=?",job);
jdbc.update("DELETE FROM jobs WHERE id=?",job);
}
if (company != null) jdbc.update("DELETE FROM companies WHERE id=?",company);
jdbc.update("DELETE FROM users WHERE id=?",user);
}
}
@Test void recentWindowUnreadUsesOneBoundedQueryOnMysql57() {
String phone = "q57-" + java.util.UUID.randomUUID().toString().substring(0, 8);
jdbc.update("INSERT INTO users (phone,role,is_active,is_verified) VALUES (?,'STUDENT',1,1)", phone);
Long user = jdbc.queryForObject("SELECT id FROM users WHERE phone=?", Long.class, phone);
java.util.List<Long> conversations = new java.util.ArrayList<>();
try {
for (int c = 0; c < 2; c++) {
String title = phone + "-" + c;
jdbc.update("INSERT INTO conversations (title,type,created_by,created_at,updated_at) "
+ "VALUES (?,'DIRECT',?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", title, user);
Long id = jdbc.queryForObject("SELECT id FROM conversations WHERE title=?", Long.class, title);
conversations.add(id);
java.util.List<Object[]> batch = new java.util.ArrayList<>();
for (int i = 0; i < 600; i++) batch.add(new Object[]{id,user,"legacy-" + i,
i % 20 == 0 ? "RECALLED" : "SENT"});
jdbc.batchUpdate("INSERT INTO messages (conversation_id,sender_user_id,content,status,created_at) "
+ "VALUES (?,?,?,?,'2026-10-15 08:00:00')", batch);
jdbc.update("INSERT INTO message_reads (message_id,reader_user_id,read_at) "
+ "SELECT id,?,CURRENT_TIMESTAMP FROM messages WHERE conversation_id=? "
+ "AND content IN ('legacy-50','legacy-150','legacy-250','legacy-350','legacy-450','legacy-550')", user,id);
}
var stats = entityManagerFactory.unwrap(org.hibernate.SessionFactory.class).getStatistics();
boolean wasEnabled = stats.isStatisticsEnabled();
stats.setStatisticsEnabled(true);
try {
long before = stats.getPrepareStatementCount();
var counts = messages.countUnreadByConversationIds(conversations, user);
assertThat(stats.getPrepareStatementCount() - before).as("两会话合计只发一条 SQL").isEqualTo(1);
assertThat(counts).hasSize(2);
assertThat(counts).extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getConversationId)
.containsExactlyInAnyOrderElementsOf(conversations);
assertThat(counts).extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount)
.containsOnly(470L);
assertThat(messages.countUnreadByConversationIds(java.util.List.of(), user)).isEmpty();
assertThat(messages.countUnreadByConversationIds(java.util.List.of(conversations.get(0)), user))
.extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount)
.containsExactly(470L);
assertThat(messages.countUnreadByConversationIds(
java.util.List.of(conversations.get(0), conversations.get(0)), user))
.extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount)
.containsExactly(470L);
} finally { stats.setStatisticsEnabled(wasEnabled); }
} finally {
for (Long id : conversations) jdbc.update("DELETE FROM conversations WHERE id=?", id);
jdbc.update("DELETE FROM users WHERE id=?", user);
}
}
@Test void fullMigrationAndSeedsValidateOnActualServerVersion() {
assertThat(jdbc.queryForObject("select version()", String.class)).startsWith("5.7.36");
assertThat(jdbc.queryForObject("select count(*) from flyway_schema_history where success=0", Long.class)).isZero();
assertThat(jdbc.queryForObject("select count(*) from flyway_schema_history where version in ('47','48','49','50','51','52','53') and success=1", Long.class)).isEqualTo(7);
assertThat(jdbc.queryForObject("select count(*) from salary_city_policy", Long.class)).isEqualTo(359);
assertThat(jdbc.queryForObject("select count(*) from occupation_skills where importance_is_proxy not in (0,1)", Long.class)).isZero();
assertThat(jdbc.queryForObject("select count(*) from onboarding_checklist_items", Long.class)).isEqualTo(12);
assertThat(jdbc.queryForObject("select count(*) from salary_calculation_history", Long.class)).isZero();
}
@Test void runtimeAndStaticOperationsRemainIdentical() throws Exception {
JsonNode runtime = mapper.readTree(mvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray());
JsonNode contract = mapper.readTree(Files.readString(Path.of("../docs/openapi-rc.json")));
assertThat(operations(runtime)).hasSize(242).isEqualTo(operations(contract));
assertThat(runtime.path("paths").size()).isEqualTo(199);
assertThat(runtime.path("paths").path("/conversations/{id}/events"))
.isEqualTo(contract.path("paths").path("/conversations/{id}/events"));
for (String field : Set.of("clientMessageId", "version")) {
String pointer = "/components/schemas/MessageResponse/properties/" + field;
assertThat(runtime.at(pointer).isMissingNode()).as(pointer).isFalse();
assertThat(contract.at(pointer)).as(pointer).isEqualTo(runtime.at(pointer));
}
String requestField = "/components/schemas/SendMessageRequest/properties/clientMessageId";
assertThat(runtime.at(requestField).isMissingNode()).isFalse();
assertThat(contract.at(requestField)).isEqualTo(runtime.at(requestField));
}
private Set<String> operations(JsonNode document) {
Set<String> result = new HashSet<>();
document.path("paths").fields().forEachRemaining(path ->
path.getValue().fieldNames().forEachRemaining(method -> {
if (Set.of("get", "post", "put", "patch", "delete", "head", "options", "trace").contains(method))
result.add(method + " " + path.getKey());
}));
return result;
}
}