package com.yau.digitalrmb.security; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; 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.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerTest { private static final String SECRET = "local-token-sso-test-secret-key-123456"; @Autowired private MockMvc mvc; @Autowired private JdbcTemplate jdbcTemplate; @Test void tokenLoginSynchronizesCrossPlatformUserAndReturnsLocalToken() throws Exception { String response = mvc.perform(post("/api/user/login") .param("TOKEN", crossPlatformToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.accessToken").isNotEmpty()) .andExpect(jsonPath("$.data.tokenType").value("Bearer")) .andExpect(jsonPath("$.data.username").value("cross001")) .andExpect(jsonPath("$.data.studentId").value("cross001")) .andExpect(jsonPath("$.data.name").value("Cross Student")) .andExpect(jsonPath("$.data.roleId").value(4)) .andExpect(jsonPath("$.data.schoolId").value("610000")) .andExpect(jsonPath("$.data.schoolName").value("Yan'an University")) .andExpect(jsonPath("$.data.classId").value("202401")) .andExpect(jsonPath("$.data.className").value("Class 1")) .andExpect(jsonPath("$.data.collegeId").value("100")) .andExpect(jsonPath("$.data.majorId").value("101")) .andReturn().getResponse().getContentAsString(); String localUserId = JsonPath.read(response, "$.data.userId"); assertThat(UUID.fromString(localUserId).toString()).isEqualTo(localUserId); assertThat(jdbcTemplate.queryForObject( "SELECT zy_user_id FROM sys_user WHERE user_id = ?", String.class, localUserId)) .isEqualTo("9001"); } @Test void parameterLoginMatchesCrossTrainingPlatformEndpointShape() throws Exception { mvc.perform(post("/api/user/login") .param("username", "tzs001") .param("passwordEncode", "123qwe")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.userId") .value("00000000-0000-0000-0000-000000000487")) .andExpect(jsonPath("$.data.username").value("tzs001")) .andExpect(jsonPath("$.data.accessToken").isNotEmpty()); } @Test void legacyPlaintextPasswordLoginUpgradesPasswordToBcrypt() throws Exception { String userId = "00000000-0000-0000-0000-000000000221"; jdbcTemplate.update("INSERT INTO sys_user " + "(user_id, student_id, password, user_name, role_id, is_deleted) " + "VALUES (?, ?, ?, ?, ?, ?)", userId, "legacy001", "123qwe", "Legacy Student", 4, 0); mvc.perform(post("/api/user/login") .param("username", "legacy001") .param("passwordEncode", "123qwe")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.userId").value(userId)) .andExpect(jsonPath("$.data.accessToken").isNotEmpty()); String upgradedPassword = jdbcTemplate.queryForObject( "SELECT password FROM sys_user WHERE user_id = ?", String.class, userId); assertThat(upgradedPassword).startsWith("$2").isNotEqualTo("123qwe"); } @Test void tokenLoginAcceptsLegacyTokenWithoutExpirationOrPassword() throws Exception { Map claims = baseClaims(); claims.remove("password"); claims.remove("exp"); mvc.perform(post("/api/user/login").param("TOKEN", signedToken(claims))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.username").value("cross001")) .andExpect(jsonPath("$.data.accessToken").isNotEmpty()); } private String crossPlatformToken() throws Exception { return signedToken(baseClaims()); } private Map baseClaims() { Map claims = new HashMap(); claims.put("userId", 9001); claims.put("username", "cross001"); claims.put("password", "token-password"); claims.put("name", "Cross Student"); claims.put("roleid", 4); claims.put("schoolId", 610000); claims.put("school", "Yan'an University"); claims.put("collegeId", 100); claims.put("collegeName", "Computer College"); claims.put("majorId", 101); claims.put("major", "Software Engineering"); claims.put("classId", 202401); claims.put("class", "Class 1"); claims.put("studentNo", "cross001"); claims.put("exp", Instant.now().plusSeconds(300).getEpochSecond()); return claims; } private String signedToken(Map claims) throws Exception { ObjectMapper mapper = new ObjectMapper(); Map header = new HashMap(); header.put("alg", "HS256"); header.put("typ", "JWT"); String unsigned = encode(mapper.writeValueAsBytes(header)) + "." + encode(mapper.writeValueAsBytes(claims)); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); return unsigned + "." + encode(mac.doFinal(unsigned.getBytes(StandardCharsets.US_ASCII))); } private String encode(byte[] value) { return Base64.getUrlEncoder().withoutPadding().encodeToString(value); } }