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.
162 lines
7.7 KiB
162 lines
7.7 KiB
package com.mydreamjob.ability;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.mydreamjob.auth.JwtTokenProvider;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
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.data.redis.core.ValueOperations;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ActiveProfiles("integration-test")
|
|
class AbilityControllerTest {
|
|
|
|
private static final AtomicLong PHONE = new AtomicLong(6000);
|
|
|
|
@Autowired private MockMvc mockMvc;
|
|
@Autowired private ObjectMapper objectMapper;
|
|
@Autowired private JwtTokenProvider jwtTokenProvider;
|
|
@Autowired private UserRepository userRepository;
|
|
|
|
@MockBean private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
private Long studentUserId;
|
|
private Long companyUserId;
|
|
private Long adminUserId;
|
|
|
|
@BeforeEach
|
|
@SuppressWarnings("unchecked")
|
|
void setUp() {
|
|
ValueOperations<String, Object> ops = mock(ValueOperations.class);
|
|
when(redisTemplate.opsForValue()).thenReturn(ops);
|
|
when(redisTemplate.hasKey(any())).thenReturn(false);
|
|
studentUserId = createUser(User.Role.STUDENT);
|
|
companyUserId = createUser(User.Role.COMPANY);
|
|
adminUserId = createUser(User.Role.ADMIN);
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
userRepository.deleteById(adminUserId);
|
|
userRepository.deleteById(companyUserId);
|
|
userRepository.deleteById(studentUserId);
|
|
}
|
|
|
|
private Long createUser(User.Role role) {
|
|
User u = userRepository.save(User.builder()
|
|
.phone("17" + String.format("%08d", PHONE.incrementAndGet())).nickname("u")
|
|
.role(role).roles(Set.of(role)).active(true).verified(true).build());
|
|
return u.getId();
|
|
}
|
|
|
|
private String token(Long userId, User.Role role) {
|
|
return com.mydreamjob.auth.support.TestAuthTokens.bearer(userId, role.name());
|
|
}
|
|
|
|
@Test
|
|
void anonymous_create_returns401() throws Exception {
|
|
mockMvc.perform(post("/abilities/attempts").contentType(MediaType.APPLICATION_JSON)
|
|
.content("{\"typeCode\":\"WORKPLACE_ABILITY\"}")).andExpect(status().isUnauthorized());
|
|
}
|
|
|
|
@Test
|
|
void studentListTypes_returns200() throws Exception {
|
|
mockMvc.perform(get("/abilities/types").header("Authorization", token(studentUserId, User.Role.STUDENT)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.[0].typeCode").value("WORKPLACE_ABILITY"));
|
|
}
|
|
|
|
@Test
|
|
void companyAndAdmin_create_returns403() throws Exception {
|
|
mockMvc.perform(post("/abilities/attempts").header("Authorization", token(companyUserId, User.Role.COMPANY))
|
|
.contentType(MediaType.APPLICATION_JSON).content("{\"typeCode\":\"WORKPLACE_ABILITY\"}"))
|
|
.andExpect(status().isForbidden());
|
|
mockMvc.perform(post("/abilities/attempts").header("Authorization", token(adminUserId, User.Role.ADMIN))
|
|
.contentType(MediaType.APPLICATION_JSON).content("{\"typeCode\":\"WORKPLACE_ABILITY\"}"))
|
|
.andExpect(status().isForbidden());
|
|
}
|
|
|
|
@Test
|
|
void fullStudentFlow_createSaveSubmitResult_repeatIdempotent_noSensitiveLeak() throws Exception {
|
|
String tk = token(studentUserId, User.Role.STUDENT);
|
|
|
|
JsonNode create = objectMapper.readTree(mockMvc.perform(post("/abilities/attempts")
|
|
.header("Authorization", tk).contentType(MediaType.APPLICATION_JSON)
|
|
.content("{\"typeCode\":\"WORKPLACE_ABILITY\",\"idempotencyKey\":\"http-key-1\"}"))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString());
|
|
long attemptId = create.path("data").path("attemptId").asLong();
|
|
assertEquals(24, create.path("data").path("questions").size());
|
|
|
|
List<Map<String, Object>> answers = new ArrayList<>();
|
|
for (JsonNode q : create.path("data").path("questions")) {
|
|
answers.add(Map.of("attemptQuestionId", q.path("attemptQuestionId").asLong(),
|
|
"selectedOptionId", "A"));
|
|
}
|
|
String body = objectMapper.writeValueAsString(Map.of("answers", answers));
|
|
|
|
mockMvc.perform(patch("/abilities/attempts/{id}/answers", attemptId)
|
|
.header("Authorization", tk).contentType(MediaType.APPLICATION_JSON).content(body))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.answeredCount").value(24));
|
|
|
|
JsonNode submitted = objectMapper.readTree(mockMvc.perform(post("/abilities/attempts/{id}/submit", attemptId)
|
|
.header("Authorization", tk))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("COMPLETED"))
|
|
.andReturn().getResponse().getContentAsString());
|
|
|
|
// 结果脱敏:完整 result JSON 不得包含题库内部字段
|
|
String submitedRaw = submitted.toString();
|
|
assertFalse(submitedRaw.contains("studentUserId"));
|
|
assertFalse(submitedRaw.contains("phone"));
|
|
assertFalse(submitedRaw.contains("email"));
|
|
assertFalse(submitedRaw.contains("answerKey"), "result must not contain answerKey");
|
|
assertFalse(submitedRaw.contains("scoringRule"), "result must not contain scoringRule");
|
|
assertFalse(submitedRaw.contains("questionCode"), "result must not contain questionCode");
|
|
assertFalse(submitedRaw.contains("competencyTag"), "result must not contain competencyTag");
|
|
assertFalse(submitedRaw.contains("source"), "result must not contain source");
|
|
assertFalse(submitedRaw.contains("difficulty"), "result must not contain difficulty");
|
|
assertFalse(submitedRaw.contains("discrimination"), "result must not contain discrimination");
|
|
assertTrue(submitedRaw.contains("disclaimer"));
|
|
assertEquals(4, submitted.path("data").path("dimensionScores").size());
|
|
|
|
// 重复提交幂等:同一综合分
|
|
JsonNode resub = objectMapper.readTree(mockMvc.perform(post("/abilities/attempts/{id}/submit", attemptId)
|
|
.header("Authorization", tk)).andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString());
|
|
assertEquals(submitted.path("data").path("overallScore").asText(),
|
|
resub.path("data").path("overallScore").asText());
|
|
|
|
// GET result 200
|
|
mockMvc.perform(get("/abilities/attempts/{id}/result", attemptId).header("Authorization", tk))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.validityStatus").value("VALID"));
|
|
}
|
|
}
|