You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
22 KiB
Java
335 lines
22 KiB
Java
package com.yau.digitalrmb.institutionidentity;
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
|
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
|
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
|
|
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
|
|
import com.yau.digitalrmb.security.application.JwtTokenService;
|
|
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.jdbc.core.JdbcTemplate;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDateTime;
|
|
import java.util.Collections;
|
|
|
|
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.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 CurrencyGenerationVerificationControllerTest {
|
|
private static final String BASE_PATH =
|
|
"/api/v1/institution-identifiers/currency-request-verifications";
|
|
private static final String USER_ID = "00000000-0000-0000-0000-000000009003";
|
|
private static final long SCHOOL_ID = 1001L;
|
|
private static final long CLASS_ID = 2001L;
|
|
|
|
@Autowired private MockMvc mvc;
|
|
@Autowired private JwtTokenService jwtTokenService;
|
|
@Autowired private JdbcTemplate jdbcTemplate;
|
|
@Autowired private InstitutionIdentifierApplicationMapper applicationMapper;
|
|
@Autowired private CurrencyGenerationRequestMapper requestMapper;
|
|
@Autowired private CurrencyGenerationVerificationMapper verificationMapper;
|
|
@Autowired private StuModuleScoreDetailsMapper scoreMapper;
|
|
@Autowired private InstitutionIdentityCryptography cryptography;
|
|
@Autowired private InstitutionKeyService keyService;
|
|
|
|
private String authorization;
|
|
private String bankPublicKey;
|
|
private String centralPrivateKey;
|
|
private long requestId;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
jdbcTemplate.update("DELETE FROM currency_generation_verification_operation_log");
|
|
jdbcTemplate.update("DELETE FROM currency_generation_verification");
|
|
jdbcTemplate.update("DELETE FROM currency_generation_request_operation_log");
|
|
jdbcTemplate.update("DELETE FROM currency_generation_request");
|
|
jdbcTemplate.update("DELETE FROM institution_identifier_operation_log");
|
|
jdbcTemplate.update("DELETE FROM institution_identifier_application");
|
|
jdbcTemplate.update("DELETE FROM institution_sm2_key_audit");
|
|
jdbcTemplate.update("DELETE FROM institution_sm2_key");
|
|
jdbcTemplate.update("DELETE FROM stu_module_score_details");
|
|
authorization = "Bearer " + jwtTokenService.issueFor(USER_ID, SCHOOL_ID, CLASS_ID,
|
|
"step-three-tester", Collections.singleton("STUDENT")).accessToken();
|
|
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
|
|
bankPublicKey = keyService.commercialBankKey(subject, "step-three-tester").getPublicKey();
|
|
centralPrivateKey = keyService.centralBankKey(subject, "step-three-tester").getPrivateKey();
|
|
requestId = insertReceivedRequest(subject);
|
|
}
|
|
|
|
@Test
|
|
void everyStepThreeButtonHasIndependentEndpointAndState() throws Exception {
|
|
String received = mvc.perform(post(BASE_PATH + "/steps/receive")
|
|
.header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("RECEIVED"))
|
|
.andReturn().getResponse().getContentAsString();
|
|
long verificationId = number(received, "$.data.verificationId");
|
|
String repeated = mvc.perform(post(BASE_PATH + "/steps/receive")
|
|
.header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
|
|
assertThat(number(repeated, "$.data.verificationId")).isEqualTo(verificationId);
|
|
assertThat(verificationMapper.selectList(null)).hasSize(1);
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNATURE_VERIFIED"));
|
|
String userInput = "用户输入的请求原文";
|
|
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(digestStep(verificationId, userInput)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"))
|
|
.andExpect(jsonPath("$.data.recomputedDigest").value(cryptography.sm3(userInput)))
|
|
.andExpect(jsonPath("$.data.digestMatches").value(org.hamcrest.Matchers.nullValue()));
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization)
|
|
.contentType("application/json").content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId))
|
|
.andExpect(jsonPath("$.data.status").value("RECEIVED"))
|
|
.andExpect(jsonPath("$.data.signatureValid").value(org.hamcrest.Matchers.nullValue()))
|
|
.andExpect(jsonPath("$.data.recomputedDigest").value(org.hamcrest.Matchers.nullValue()));
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNATURE_VERIFIED"));
|
|
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(digestStep(verificationId,
|
|
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED"));
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_MATCHED"))
|
|
.andExpect(jsonPath("$.data.digestMatches").value(true));
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.totalQuota").value(50000.00))
|
|
.andExpect(jsonPath("$.data.usedQuota").value(0))
|
|
.andExpect(jsonPath("$.data.remainingQuota").value(50000.00))
|
|
.andExpect(jsonPath("$.data.currentAmount").value(1589.85))
|
|
.andExpect(jsonPath("$.data.quotaSufficient").value(true))
|
|
.andExpect(jsonPath("$.data.verificationId").doesNotExist())
|
|
.andExpect(jsonPath("$.data.status").doesNotExist());
|
|
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(digestStep(verificationId,
|
|
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"));
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED"))
|
|
.andExpect(jsonPath("$.data.digestMatches").value(true));
|
|
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId, "privateKey", centralPrivateKey)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CONFIRMED"))
|
|
.andExpect(jsonPath("$.data.centralBankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
|
|
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.nullValue()));
|
|
mvc.perform(post(BASE_PATH + "/steps/package-response").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
|
|
.andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.containsString(
|
|
"\"status\":\"CONFIRMED\"")));
|
|
mvc.perform(post(BASE_PATH + "/steps/return-response").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
|
|
mvc.perform(get(BASE_PATH + "/current").header("Authorization", authorization))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId))
|
|
.andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
|
|
mvc.perform(get(BASE_PATH).header("Authorization", authorization).param("verificationId", String.valueOf(verificationId)))
|
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED"));
|
|
|
|
CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId);
|
|
assertThat(stored.getConfirmationMessage()).contains("CONFIRMED", "cbSignature", "confirmTime");
|
|
assertThat(stored.getDeleted()).isFalse();
|
|
}
|
|
|
|
@Test
|
|
void matchDigestWithoutCalculatedDigestReturnsReminderWithoutCountingError() throws Exception {
|
|
String received = mvc.perform(post(BASE_PATH + "/steps/receive")
|
|
.header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
long verificationId = number(received, "$.data.verificationId");
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isBadRequest())
|
|
.andExpect(jsonPath("$.message").value("还未进行摘要运算,请先运算摘要一"));
|
|
|
|
assertThat(scoreMapper.selectCount(new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
|
|
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID))).isZero();
|
|
}
|
|
|
|
@Test
|
|
void accumulatesDigestMismatchErrors() throws Exception {
|
|
String received = mvc.perform(post(BASE_PATH + "/steps/receive")
|
|
.header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
long verificationId = number(received, "$.data.verificationId");
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey)))
|
|
.andExpect(status().isOk());
|
|
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(digestStep(verificationId, "错误的请求原文")))
|
|
.andExpect(status().isOk());
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isBadRequest())
|
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
|
|
"摘要一和摘要二不一致")))
|
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isBadRequest())
|
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
|
|
|
|
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
|
|
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
|
|
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
|
|
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
|
|
assertThat(score.getCompletionStatus()).isEqualTo("2");
|
|
CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId);
|
|
assertThat(stored.getStatus()).isEqualTo("DIGEST_CALCULATED");
|
|
assertThat(stored.getDigestMatches()).isNull();
|
|
}
|
|
|
|
@Test
|
|
void accumulatesWrongStepThreePublicAndPrivateKeys() throws Exception {
|
|
String received = mvc.perform(post(BASE_PATH + "/steps/receive")
|
|
.header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content("{\"requestId\":" + requestId + "}"))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
long verificationId = number(received, "$.data.verificationId");
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content(step(verificationId, "publicKey", "BANK_SECOND_SM2_KEY")))
|
|
.andExpect(status().isBadRequest())
|
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content(step(verificationId, "publicKey", bankPublicKey)))
|
|
.andExpect(status().isOk());
|
|
mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(digestStep(verificationId,
|
|
"ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100")))
|
|
.andExpect(status().isOk());
|
|
mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk());
|
|
mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization)
|
|
.contentType("application/json").content(step(verificationId)))
|
|
.andExpect(status().isOk());
|
|
|
|
mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization)
|
|
.contentType("application/json")
|
|
.content(step(verificationId, "privateKey", InstitutionKeyService.CENTRAL_FIRST_KEY)))
|
|
.andExpect(status().isBadRequest())
|
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
|
|
|
|
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
|
|
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
|
|
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
|
|
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
|
|
assertThat(score.getCompletionStatus()).isEqualTo("2");
|
|
}
|
|
|
|
@Test
|
|
void globalQuotaCountsOtherSubjectsAndIgnoresDeletedRequests() {
|
|
CurrencyGenerationRequestEntity active = quotaRequest("other-user", new BigDecimal("42000.00"), false);
|
|
requestMapper.insert(active);
|
|
CurrencyGenerationRequestEntity deleted = quotaRequest("deleted-user", new BigDecimal("7000.00"), true);
|
|
requestMapper.insert(deleted);
|
|
|
|
assertThat(requestMapper.sumActiveAmountExcluding(requestId)).isEqualByComparingTo("42000.00");
|
|
assertThat(requestMapper.sumActiveAmountExcluding(active.getId())).isEqualByComparingTo("1589.85");
|
|
}
|
|
|
|
private CurrencyGenerationRequestEntity quotaRequest(String userId, BigDecimal amount, boolean deleted) {
|
|
LocalDateTime now = LocalDateTime.now();
|
|
CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity();
|
|
request.setIdentifierApplicationId(requestMapper.selectById(requestId).getIdentifierApplicationId());
|
|
request.setInstitutionIdentifier("ORG_GLOBAL_QUOTA");
|
|
request.setFullInstitutionIdentifier("ORG_GLOBAL_QUOTA"); request.setAmount(amount);
|
|
request.setDeliveryNodeCode("SYS_GLOBAL"); request.setRequestTimestamp("20260806210000");
|
|
request.setRequestOriginalText("quota"); request.setRequestDigest("A"); request.setBankSignature("B");
|
|
request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY); request.setRequestMessage("{}");
|
|
request.setStatus("RECEIVED"); request.setUserId(userId); request.setSchoolId(9999L); request.setClassId(9999L);
|
|
request.setCreatedAt(now); request.setUpdatedAt(now); request.setCreatedBy(userId); request.setUpdatedBy(userId);
|
|
request.setDeleted(deleted);
|
|
return request;
|
|
}
|
|
|
|
private long insertReceivedRequest(InstitutionKeySubject subject) {
|
|
LocalDateTime now = LocalDateTime.now();
|
|
InstitutionIdentifierApplicationEntity application = new InstitutionIdentifierApplicationEntity();
|
|
application.setBankCode("BKCHCNBJ30003");
|
|
application.setUserId(USER_ID); application.setSchoolId(SCHOOL_ID); application.setClassId(CLASS_ID);
|
|
application.setRequestTimestamp("20260806190000");
|
|
application.setOriginalText("BKCHCNBJ30003|20260806190000");
|
|
application.setStatus("FEEDBACKED"); application.setTrainingRound(1); application.setScoringCriteria(0);
|
|
application.setCreatedAt(now); application.setUpdatedAt(now);
|
|
application.setCreatedBy("step-three-tester"); application.setUpdatedBy("step-three-tester");
|
|
application.setDeleted(false); applicationMapper.insert(application);
|
|
|
|
String original = "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100";
|
|
String digest = cryptography.sm3(original);
|
|
CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity();
|
|
request.setIdentifierApplicationId(application.getId()); request.setInstitutionIdentifier("ORG_3A4B5C6D7E8F");
|
|
request.setFullInstitutionIdentifier("ORG_3A4B5C6D7E8F"); request.setAmount(new BigDecimal("1589.85"));
|
|
request.setDeliveryNodeCode("SYS_DC_002"); request.setRequestTimestamp("20260806190100");
|
|
request.setRequestOriginalText(original); request.setRequestDigest(digest);
|
|
request.setBankSignature(cryptography.sign(keyService.commercialBankSecondPrivateKeyOrNull(subject), digest));
|
|
request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY);
|
|
request.setRequestMessage("{\"digest\":\"" + digest + "\"}"); request.setStatus("RECEIVED");
|
|
request.setUserId(USER_ID); request.setSchoolId(SCHOOL_ID); request.setClassId(CLASS_ID);
|
|
request.setCreatedAt(now); request.setUpdatedAt(now);
|
|
request.setCreatedBy("step-three-tester"); request.setUpdatedBy("step-three-tester"); request.setDeleted(false);
|
|
requestMapper.insert(request);
|
|
return request.getId();
|
|
}
|
|
|
|
private static String step(long id) { return "{\"verificationId\":" + id + "}"; }
|
|
private static String digestStep(long id, String originalText) {
|
|
return "{\"verificationId\":" + id + ",\"requestOriginalText\":\"" + originalText + "\"}";
|
|
}
|
|
private static String step(long id, String key, String value) {
|
|
return "{\"verificationId\":" + id + ",\"" + key + "\":\"" + value + "\"}";
|
|
}
|
|
private static long number(String json, String path) {
|
|
return com.jayway.jsonpath.JsonPath.<Number>read(json, path).longValue();
|
|
}
|
|
}
|