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