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.
 
 
 

325 lines
13 KiB

package com.mydreamjob.skill;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.skill.domain.SkillIndustry;
import com.mydreamjob.skill.domain.SkillOccupation;
import com.mydreamjob.skill.dto.SkillCloudItem;
import com.mydreamjob.skill.dto.SkillCloudResponse;
import com.mydreamjob.skill.dto.SkillCloudSkillProjection;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.Sequence;
import org.hibernate.mapping.Table;
import org.hibernate.tool.schema.spi.SchemaFilter;
import org.hibernate.tool.schema.spi.SchemaFilterProvider;
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.TestPropertySource;
import com.mydreamjob.skill.repository.OccupationSkillRepository;
import com.mydreamjob.skill.repository.SkillCategoryRepository;
import com.mydreamjob.skill.repository.SkillIndustryRepository;
import com.mydreamjob.skill.repository.SkillOccupationRepository;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowableOfType;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:integrationdb_skill_query;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"spring.jpa.hibernate.ddl-auto=validate",
"spring.jpa.properties.hibernate.hbm2ddl.schema_filter_provider=com.mydreamjob.skill.SkillCloudQueryServiceTest$SkillQuerySchemaFilterProvider"
})
@ActiveProfiles("integration-test")
@Transactional
class SkillCloudQueryServiceTest {
@Autowired
private SkillCloudQueryService service;
@Autowired
private JdbcTemplate jdbcTemplate;
@MockBean
private RedisTemplate<String, Object> redisTemplate;
@Test
void returnsAllSeedSkillsForIndustryAndOccupationWithContinuousRanks() {
SkillCloudResponse response = service.query(1L, 1L, null);
assertThat(response.industry().id()).isEqualTo(1L);
assertThat(response.industry().code()).isEqualTo("INTERNET");
assertThat(response.industry().name()).isEqualTo("互联网/信息技术");
assertThat(response.occupation().id()).isEqualTo(1L);
assertThat(response.occupation().code()).isEqualTo("FRONTEND_DEVELOPER");
assertThat(response.occupation().name()).isEqualTo("前端开发工程师");
assertThat(response.category()).isNull();
assertThat(response.total()).isEqualTo(15);
assertThat(response.items()).hasSize(15);
assertThat(response.items())
.extracting(SkillCloudItem::rank)
.containsExactlyElementsOf(IntStream.rangeClosed(1, 15).boxed().toList());
assertThat(response.items())
.extracting(SkillCloudItem::skillId)
.doesNotHaveDuplicates();
}
@Test
void sortsByHeatThenNameAndKeepsNullHeatLast() {
SkillCloudResponse response = service.query(1L, 1L, 104L);
assertThat(response.category().id()).isEqualTo(104L);
assertThat(response.category().code()).isEqualTo("TOOLS");
assertThat(response.category().name()).isEqualTo("工程化工具");
assertThat(response.items())
.extracting(SkillCloudItem::nameZh)
.containsExactly("Gradle", "Node.js", "Figma");
assertThat(response.items())
.extracting(SkillCloudItem::heat)
.containsExactly(new BigDecimal("4.88"), new BigDecimal("4.88"), null);
assertThat(response.items().get(response.items().size() - 1).heat()).isNull();
SkillCloudResponse all = service.query(1L, 1L, null);
assertThat(all.items().stream()
.map(SkillCloudItem::heat)
.filter(java.util.Objects::nonNull)
.toList())
.isSortedAccordingTo(java.util.Comparator.reverseOrder());
assertThat(all.items().get(all.items().size() - 1).heat()).isNull();
assertThat(all.items().subList(0, all.items().size() - 1))
.allSatisfy(item -> assertThat(item.heat()).isNotNull());
}
@Test
void comparatorUsesSkillIdWhenEqualKeysArriveInReverseOrder() {
SkillIndustry industry = new SkillIndustry();
industry.setId(1L);
industry.setCode("INTERNET");
industry.setNameZh("互联网");
SkillOccupation occupation = new SkillOccupation();
occupation.setId(1L);
occupation.setIndustryId(1L);
occupation.setCode("FRONTEND_DEVELOPER");
occupation.setNameZh("前端开发工程师");
SkillIndustryRepository industryRepository = mock(SkillIndustryRepository.class);
when(industryRepository.findById(1L)).thenReturn(Optional.of(industry));
SkillOccupationRepository occupationRepository = mock(SkillOccupationRepository.class);
when(occupationRepository.findById(1L)).thenReturn(Optional.of(occupation));
OccupationSkillRepository occupationSkillRepository = mock(OccupationSkillRepository.class);
when(occupationSkillRepository.findSkillProjectionsByOccupationId(1L))
.thenReturn(List.of(
projection(9002L, "TIE_SKILL_HIGH"),
projection(9001L, "TIE_SKILL_LOW")));
SkillCloudQueryService isolatedService = new SkillCloudQueryService(
industryRepository,
occupationRepository,
mock(SkillCategoryRepository.class),
occupationSkillRepository);
SkillCloudResponse response = isolatedService.query(1L, 1L, null);
assertThat(response.items())
.extracting(SkillCloudItem::skillId)
.containsExactly(9001L, 9002L);
}
private SkillCloudSkillProjection projection(Long skillId, String skillCode) {
return new SkillCloudSkillProjection(
skillId,
skillCode,
"同名技能",
"Tie Skill",
"O*NET",
"TEST|Tie Skill",
104L,
"工程化工具",
new BigDecimal("4.88"),
"15-1254.00",
"4.A.3.b.1",
true,
1,
1
);
}
@Test
void categoryFilterReturnsOnlyTheRequestedCategoryAndOmittedCategoryReturnsMore() {
SkillCloudResponse all = service.query(1L, 1L, null);
SkillCloudResponse filtered = service.query(1L, 1L, 101L);
assertThat(filtered.total()).isEqualTo(2);
assertThat(filtered.items())
.extracting(SkillCloudItem::categoryId)
.containsOnly(101L);
assertThat(filtered.items())
.extracting(SkillCloudItem::skillId)
.containsExactlyInAnyOrder(3L, 4L);
assertThat(all.total()).isGreaterThan(filtered.total());
}
@Test
void returnsEmptySuccessForLegalCategoryWithoutSkills() {
jdbcTemplate.update("""
INSERT INTO skill_categories
(id, occupation_id, code, name_zh, suggestion_template_code, sort_order)
VALUES
(9101, 1, 'EMPTY_CATEGORY', '空分类', 'GENERAL', 99)
""");
SkillCloudResponse response = service.query(1L, 1L, 9101L);
assertThat(response.category().id()).isEqualTo(9101L);
assertThat(response.category().name()).isEqualTo("空分类");
assertThat(response.total()).isZero();
assertThat(response.items()).isEmpty();
}
@Test
void mapsSkillAndHeatProvenanceFieldsFromTheDatabase() {
SkillCloudResponse frameworks = service.query(1L, 1L, 103L);
SkillCloudItem react = frameworks.items().stream()
.filter(item -> item.skillId().equals(26L))
.findFirst()
.orElseThrow();
Map<String, Object> row = jdbcTemplate.queryForMap("""
SELECT os.importance, os.importance_source_code, os.importance_element_id,
os.importance_is_proxy
FROM occupation_skills os
WHERE os.occupation_id = 1 AND os.skill_id = 26
""");
assertThat(react.rank()).isEqualTo(1);
assertThat(react.skillCode()).isEqualTo("REACT");
assertThat(react.nameZh()).isEqualTo("React");
assertThat(react.nameEn()).isEqualTo("React");
assertThat(react.categoryId()).isEqualTo(103L);
assertThat(react.categoryName()).isEqualTo("前端框架");
assertThat(react.sourceType()).isEqualTo("O*NET");
assertThat(react.sourceCode()).isEqualTo("15-1254.00|React|2.E.7.h");
assertThat(react.heat()).isEqualByComparingTo((BigDecimal) row.get("importance"));
assertThat(react.heatSourceCode()).isEqualTo(row.get("importance_source_code"));
assertThat(react.heatElementId()).isEqualTo(row.get("importance_element_id"));
assertThat(react.heatProxy()).isEqualTo(toBoolean(row.get("importance_is_proxy")));
SkillCloudResponse languages = service.query(1L, 1L, 101L);
SkillCloudItem javascript = languages.items().stream()
.filter(item -> item.skillId().equals(3L))
.findFirst()
.orElseThrow();
assertThat(javascript.sourceType()).isEqualTo("ESCO");
assertThat(javascript.sourceCode())
.isEqualTo("http://data.europa.eu/esco/skill/3cd569a2-4f88-4c1e-9995-8dce8c5e51a7");
assertThat(javascript.heatSourceCode()).isEqualTo("15-1254.00");
}
@Test
void rejectsMissingOrNonPositiveIdentifiersWithBadRequest() {
assertBadRequest(() -> service.query(null, 1L, null), "industryId");
assertBadRequest(() -> service.query(0L, 1L, null), "industryId");
assertBadRequest(() -> service.query(-1L, 1L, null), "industryId");
assertBadRequest(() -> service.query(1L, null, null), "occupationId");
assertBadRequest(() -> service.query(1L, 0L, null), "occupationId");
assertBadRequest(() -> service.query(1L, -1L, null), "occupationId");
assertBadRequest(() -> service.query(1L, 1L, 0L), "categoryId");
assertBadRequest(() -> service.query(1L, 1L, -1L), "categoryId");
}
@Test
void rejectsUnknownOrMismatchedTaxonomyNodesWithBadRequest() {
assertBadRequest(() -> service.query(999L, 1L, null), "industry not found");
assertBadRequest(() -> service.query(1L, 999L, null), "occupation not found");
assertBadRequest(() -> service.query(2L, 1L, null), "occupation does not belong");
assertBadRequest(() -> service.query(1L, 1L, 201L), "category does not belong");
assertBadRequest(() -> service.query(1L, 1L, 9999L), "category not found");
}
private boolean toBoolean(Object value) {
if (value instanceof Boolean booleanValue) {
return booleanValue;
}
return ((Number) value).intValue() != 0;
}
private void assertBadRequest(ThrowingCallable action, String detailFragment) {
BusinessException exception = catchThrowableOfType(action, BusinessException.class);
assertThat(exception).isNotNull();
assertThat(exception.getErrorCodeInt()).isEqualTo(ErrorCode.BAD_REQUEST.getCode());
assertThat(exception.getErrorMessage()).contains(detailFragment);
}
public static class SkillQuerySchemaFilterProvider implements SchemaFilterProvider {
private static final Set<String> VALIDATED_TABLES = Set.of(
"skill_source_versions",
"skill_industries",
"skill_occupations",
"skill_categories",
"skills",
"skill_aliases",
"occupation_skills"
);
@Override
public SchemaFilter getCreateFilter() {
return SchemaFilter.ALL;
}
@Override
public SchemaFilter getDropFilter() {
return SchemaFilter.ALL;
}
@Override
public SchemaFilter getTruncatorFilter() {
return SchemaFilter.ALL;
}
@Override
public SchemaFilter getMigrateFilter() {
return SchemaFilter.ALL;
}
@Override
public SchemaFilter getValidateFilter() {
return new SchemaFilter() {
@Override
public boolean includeNamespace(Namespace namespace) {
return true;
}
@Override
public boolean includeTable(Table table) {
return VALIDATED_TABLES.contains(table.getName().toLowerCase(Locale.ROOT));
}
@Override
public boolean includeSequence(Sequence sequence) {
return false;
}
};
}
}
}