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.
digital-rmb-backend/src/test/java/com/yau/digitalrmb/institutionidentity/InstitutionIdentifierContro...

919 lines
53 KiB
Java

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.yau.digitalrmb.institutionidentity;
import com.jayway.jsonpath.JsonPath;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionPrivateKeyCipher;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyMapper;
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.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
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 InstitutionIdentifierControllerTest {
private static final String BASE_PATH = "/api/v1/institution-identifiers";
private static final String USER_ID = "00000000-0000-0000-0000-000000009001";
@Autowired
private MockMvc mvc;
@Autowired
private JwtTokenService jwtTokenService;
@Autowired
private InstitutionIdentifierApplicationMapper applicationMapper;
@Autowired
private InstitutionIdentifierOperationLogMapper operationLogMapper;
@Autowired
private InstitutionIdentifierTrainingErrorMapper trainingErrorMapper;
@Autowired
private InstitutionSm2KeyMapper keyMapper;
@Autowired
private InstitutionPrivateKeyCipher privateKeyCipher;
@Autowired
private InstitutionKeyService keyService;
@Autowired
private StuModuleScoreDetailsMapper scoreDetailsMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
private String authorization;
private String keyAuthorization;
private String tokenWithoutSubjectClaims;
@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_training_error");
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");
keyAuthorization = "Bearer " + jwtTokenService.issueFor(USER_ID, 1001L, 2001L,
"institution-key-tester", Collections.singleton("STUDENT")).accessToken();
authorization = keyAuthorization;
tokenWithoutSubjectClaims = "Bearer " + jwtTokenService.issueFor("institution-api-tester").accessToken();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, 1001L, 2001L);
keyService.commercialBankPublicKey(subject, "institution-key-tester");
keyService.centralBankPublicKey(subject, "institution-key-tester");
}
@Test
void resetTrainingLogicallyDeletesStepDataButPreservesKeysAndAllowsRestart() throws Exception {
digest("BKCHCNBJ20991");
Integer keyCountBefore = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_sm2_key WHERE deleted = FALSE", Integer.class);
mvc.perform(post(BASE_PATH + "/training/reset")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.reset").value(true))
.andExpect(jsonPath("$.data.keysPreserved").value(true));
Integer activeApplications = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_identifier_application WHERE user_id = ? AND school_id = ? AND class_id = ? AND deleted = FALSE",
Integer.class, USER_ID, 1001L, 2001L);
Integer deletedApplications = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_identifier_application WHERE user_id = ? AND school_id = ? AND class_id = ? AND deleted = TRUE",
Integer.class, USER_ID, 1001L, 2001L);
Integer keyCountAfter = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_sm2_key WHERE deleted = FALSE", Integer.class);
assertThat(activeApplications).isZero();
assertThat(deletedApplications).isEqualTo(1);
assertThat(keyCountAfter).isEqualTo(keyCountBefore);
digest("BKCHCNBJ20992");
mvc.perform(get(BASE_PATH + "/pages/commercial-bank-application")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.bankCode").value("BKCHCNBJ20992"))
.andExpect(jsonPath("$.data.status").value("DIGESTED"));
}
@Test
void repeatedDigestUpdatesLatestRoundInsteadOfCreatingNewApplication() throws Exception {
digest("BKCHCNBJ20995");
Long firstId = jdbcTemplate.queryForObject(
"SELECT id FROM institution_identifier_application WHERE user_id = ? AND deleted = FALSE",
Long.class, USER_ID);
digest("BKCHCNBJ20996");
Integer activeCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_identifier_application WHERE user_id = ? AND deleted = FALSE",
Integer.class, USER_ID);
Long currentId = jdbcTemplate.queryForObject(
"SELECT id FROM institution_identifier_application WHERE user_id = ? AND deleted = FALSE",
Long.class, USER_ID);
String currentBankCode = jdbcTemplate.queryForObject(
"SELECT bank_code FROM institution_identifier_application WHERE id = ?", String.class, currentId);
assertThat(activeCount).isEqualTo(1);
assertThat(currentId).isEqualTo(firstId);
assertThat(currentBankCode).isEqualTo("BKCHCNBJ20996");
}
@Test
void latestRoundBlocksFallbackToOlderRoundWithExpectedStatus() throws Exception {
digest("BKCHCNBJ20993");
digest("BKCHCNBJ20994");
String privateKey = keyService.commercialBankKey(
new InstitutionKeySubject(USER_ID, 1001L, 2001L), "institution-key-tester").getPrivateKey();
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", org.hamcrest.Matchers.containsString("当前最新一轮状态:SIGNED")));
}
@Test
void pageQueriesReturnFixedStructureAndNullForUnavailableApplicationFields() throws Exception {
mvc.perform(get(BASE_PATH + "/pages/commercial-bank-application")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationId").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.status").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.bankCode").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.commercialBankSecondPrivateKey")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
mvc.perform(get(BASE_PATH + "/pages/central-bank-verification")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationId").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.originalText").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.commercialBankSecondPublicKey")
.value(org.hamcrest.Matchers.matchesPattern("04[0-9A-F]{128}")))
.andExpect(jsonPath("$.data.centralBankFirstPrivateKey")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
digest("BKCHCNBJ20105");
mvc.perform(get(BASE_PATH + "/pages/commercial-bank-application")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
.andExpect(jsonPath("$.data.bankCode").value("BKCHCNBJ20105"))
.andExpect(jsonPath("$.data.timestamp").value("20260730143218"))
.andExpect(jsonPath("$.data.originalText")
.value("BKCHCNBJ20105|20260730143218"))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.bankSignature").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.applicationMessage").value(org.hamcrest.Matchers.nullValue()));
}
@Test
void bankAndCentralKeyEndpointsPersistEncryptedPrivateKeysForTokenSubject() throws Exception {
String bankResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
.header("Authorization", keyAuthorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.BANK_SECOND_KEY))
.andExpect(jsonPath("$.data.owner").value("COMMERCIAL_BANK"))
.andExpect(jsonPath("$.data.userId").value(USER_ID))
.andExpect(jsonPath("$.data.schoolId").value(1001))
.andExpect(jsonPath("$.data.classId").value(2001))
.andExpect(jsonPath("$.data.publicKey").value(org.hamcrest.Matchers.matchesPattern("04[0-9A-F]{128}")))
.andExpect(jsonPath("$.data.privateKey").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andReturn().getResponse().getContentAsString();
mvc.perform(post(BASE_PATH + "/keys/central-bank")
.header("Authorization", keyAuthorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.CENTRAL_FIRST_KEY))
.andExpect(jsonPath("$.data.owner").value("CENTRAL_BANK"))
.andExpect(jsonPath("$.data.publicKey").value(org.hamcrest.Matchers.matchesPattern("04[0-9A-F]{128}")))
.andExpect(jsonPath("$.data.privateKey").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
List<InstitutionSm2KeyEntity> keys = keyMapper.selectList(null);
assertThat(keys).hasSize(2);
String returnedPrivateKey = JsonPath.read(bankResponse, "$.data.privateKey");
InstitutionSm2KeyEntity bankKey = keys.stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
assertThat(bankKey.getEncryptedPrivateKey()).isNotEqualTo(returnedPrivateKey);
assertThat(privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey())).isEqualTo(returnedPrivateKey);
}
@Test
void allInstitutionTrainingTablesHaveLogicalDeleteAndMapperDeleteHidesRows() throws Exception {
String[] tables = {
"institution_identifier_application",
"institution_identifier_operation_log",
"institution_identifier_training_error",
"institution_sm2_key",
"institution_sm2_key_audit",
"currency_generation_request",
"currency_generation_request_operation_log",
"currency_generation_verification",
"currency_generation_verification_operation_log"
};
for (String table : tables) {
Integer columnCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
+ "WHERE LOWER(TABLE_NAME) = ? AND LOWER(COLUMN_NAME) = 'deleted'",
Integer.class, table);
assertThat(columnCount).as(table + " must have deleted column").isEqualTo(1);
}
mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20100|20260730143218\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG\"}"))
.andExpect(status().isBadRequest());
InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0);
assertThat(trainingErrorMapper.deleteById(error.getId())).isEqualTo(1);
assertThat(trainingErrorMapper.selectById(error.getId())).isNull();
Boolean deleted = jdbcTemplate.queryForObject(
"SELECT deleted FROM institution_identifier_training_error WHERE id = ?",
Boolean.class, error.getId());
assertThat(deleted).isTrue();
}
@Test
void signAcceptsCommercialBankSecondPrivateKeyReturnedByKeyEndpoint() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20101|20260730143218\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
}
@Test
void signRejectsWrongPrivateKeyWithoutPersistingPrivateKeyValue() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20102|20260730143218\"}"))
.andExpect(status().isOk());
String malformedPrivateKey = "1173B8C5C558BAE5C2C9EB89FD5C30BCB7DA8B478B";
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + malformedPrivateKey + "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"签名必须选择商业银行第二私钥")))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
String wrongPrivateKey = "0000000000000000000000000000000000000000000000000000000000000001";
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + wrongPrivateKey + "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
InstitutionIdentifierApplicationEntity application = applicationMapper.selectOne(null);
assertThat(application.getScoringCriteria()).isEqualTo(2);
List<InstitutionIdentifierTrainingErrorEntity> errors = trainingErrorMapper.selectList(null);
assertThat(errors).hasSize(2);
assertThat(errors).extracting(InstitutionIdentifierTrainingErrorEntity::getErrorType,
InstitutionIdentifierTrainingErrorEntity::getProvidedKeyId)
.containsOnly(
org.assertj.core.groups.Tuple.tuple("WRONG_PRIVATE_KEY", null),
org.assertj.core.groups.Tuple.tuple("WRONG_PRIVATE_KEY", null));
}
@Test
void logicalDeleteIsScopedToTokenSubjectAndAllowsRegeneration() throws Exception {
String firstResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
.header("Authorization", keyAuthorization))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
Number firstRecordId = JsonPath.read(firstResponse, "$.data.recordId");
keyService.logicalDelete(new InstitutionKeySubject(USER_ID, 1001L, 2001L),
InstitutionKeyService.BANK_SECOND_KEY, "institution-key-tester");
Integer deleted = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_sm2_key WHERE id = ? AND deleted = TRUE",
Integer.class, firstRecordId.longValue());
assertThat(deleted).isEqualTo(1);
String regeneratedResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
.header("Authorization", keyAuthorization))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
Number regeneratedRecordId = JsonPath.read(regeneratedResponse, "$.data.recordId");
assertThat(regeneratedRecordId.longValue()).isNotEqualTo(firstRecordId.longValue());
assertThat(keyMapper.selectList(null)).hasSize(2);
}
@Test
void keyEndpointRejectsTokenWithoutSchoolAndClassClaims() throws Exception {
mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
.header("Authorization", tokenWithoutSubjectClaims))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.message").value("Token缺少userId"));
}
@Test
void protectedStepRejectsMissingJwt() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/digest")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"BKCHCNBJ20001|20260730143218\"}"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value("UNAUTHORIZED"));
}
@Test
void signWithoutCurrentDigestedRoundReturnsResourceNotFoundAndDoesNotCreateErrorRecord() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG\"}"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("RESOURCE_NOT_FOUND"))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"当前没有待签名的本轮机构标识数据")));
assertThat(applicationMapper.selectList(null)).isEmpty();
assertThat(trainingErrorMapper.selectList(null)).isEmpty();
assertThat(operationLogMapper.selectList(null)).isEmpty();
}
@Test
void wrongKeysIncreaseScoringCriteriaAndRecordErrorSequence() throws Exception {
long applicationId = digest("BKCHCNBJ20004");
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG_PRIVATE_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"ANOTHER_WRONG_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId);
assertThat(application.getStatus()).isEqualTo("DIGESTED");
assertThat(application.getScoringCriteria()).isEqualTo(2);
assertThat(trainingErrorMapper.selectList(null))
.filteredOn(error -> error.getApplicationId().equals(applicationId))
.extracting(InstitutionIdentifierTrainingErrorEntity::getErrorSequence,
InstitutionIdentifierTrainingErrorEntity::getProvidedKeyId)
.containsExactly(
org.assertj.core.groups.Tuple.tuple(1, null),
org.assertj.core.groups.Tuple.tuple(2, null));
StuModuleScoreDetailsEntity score = onlyScoreDetail();
assertThat(score.getMoudule()).isEqualTo("中国人民银行生成数字货币");
assertThat(score.getLearningProjects()).isEqualTo("实验实训");
assertThat(score.getSerialNumber()).isEqualTo(3);
assertThat(score.getCompletionStatus()).isEqualTo("2");
assertThat(score.getScoringCriteria()).isEqualTo("0.5");
assertThat(score.getSchedule()).isZero();
assertThat(score.getScoreProject()).isZero();
}
@Test
void errorsAccumulateOnLatestRoundWhenDigestIsResubmitted() throws Exception {
digest("BKCHCNBJ20006");
mvc.perform(post(BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG_ONE\"}"))
.andExpect(status().isBadRequest());
mvc.perform(post(BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG_TWO\"}"))
.andExpect(status().isBadRequest());
assertThat(onlyScoreDetail().getCompletionStatus()).isEqualTo("2");
digest("BKCHCNBJ20006");
mvc.perform(post(BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG_AFTER_RESUBMIT\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第3次错误")));
StuModuleScoreDetailsEntity score = onlyScoreDetail();
assertThat(score.getCompletionStatus()).isEqualTo("3");
assertThat(score.getSchedule()).isZero();
assertThat(score.getScoreProject()).isZero();
}
@Test
void digestResubmissionKeepsSameApplicationAndTrainingRound() throws Exception {
long firstApplicationId = digest("BKCHCNBJ20005");
long secondApplicationId = digest("BKCHCNBJ20005");
assertThat(secondApplicationId).isEqualTo(firstApplicationId);
InstitutionIdentifierApplicationEntity current = applicationMapper.selectById(firstApplicationId);
assertThat(current.getTrainingRound()).isEqualTo(1);
assertThat(current.getScoringCriteria()).isZero();
Integer activeCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM institution_identifier_application WHERE user_id = ? AND deleted = FALSE",
Integer.class, USER_ID);
assertThat(activeCount).isEqualTo(1);
}
@Test
void completeApiWorkflowUsesCurrentTrainingRoundAndPersistsIssuedResult() throws Exception {
long applicationId = digest("BKCHCNBJ20003");
mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"商业银行第二私钥签名成功后才会出现待确认数据")));
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.statusText").value("待确认"))
.andExpect(jsonPath("$.data.applicationStatus").value("SIGNED"))
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(org.hamcrest.Matchers.nullValue()));
mvc.perform(post(BASE_PATH + "/steps/package")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"));
mvc.perform(post(BASE_PATH + "/steps/send")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SUBMITTED"))
.andExpect(jsonPath("$.data.sentToCentralBank").value(true));
mvc.perform(post(BASE_PATH + "/applications/decryption-verification")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"publicKey\":\"" + bankPublicKey() + "\",\"digest\":\""
+ applicationMapper.selectById(applicationId).getDigest() + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("VERIFIED"))
.andExpect(jsonPath("$.data.verificationKeyId")
.value(InstitutionKeyService.BANK_SECOND_KEY))
.andExpect(jsonPath("$.data.signatureValid").value(true))
.andExpect(jsonPath("$.data.digestMatches").value(true))
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h2").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.digestComparison").value("H1 = H2"))
.andExpect(jsonPath("$.data.verificationProcess").value(org.hamcrest.Matchers.containsString(
"重新进行SM3哈希运算得到H1")))
.andExpect(jsonPath("$.data.signatureConclusion").value(org.hamcrest.Matchers.containsString(
"身份真实性验证通过")))
.andExpect(jsonPath("$.data.integrityConclusion").value(org.hamcrest.Matchers.containsString(
"数据完整性验证通过")));
mvc.perform(get(BASE_PATH + "/applications/verification-process")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationStatus").value("VERIFIED"))
.andExpect(jsonPath("$.data.originalText").value("BKCHCNBJ20003|20260730143218"))
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h2").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.digestComparison").value("H1 = H2"))
.andExpect(jsonPath("$.data.signatureValid").value(true))
.andExpect(jsonPath("$.data.digestMatches").value(true))
.andExpect(jsonPath("$.data.verified").value(true))
.andExpect(jsonPath("$.data.identityVerificationTool").value("商业银行第二公钥"))
.andExpect(jsonPath("$.data.integrityVerificationTool").value("SM3"));
mvc.perform(post(BASE_PATH + "/applications/confirmation")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(BASE_PATH + "/applications/final-identifier")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationStatus").value("ISSUED"))
.andExpect(jsonPath("$.data.feedbackStatus").value(1))
.andExpect(jsonPath("$.data.feedbackStatusText").value("待反馈"))
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(org.hamcrest.Matchers.startsWith("ORG_")))
.andExpect(jsonPath("$.data.fullInstitutionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.statusText").value("待确认"))
.andExpect(jsonPath("$.data.applicationStatus").value("ISSUED"))
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(org.hamcrest.Matchers.nullValue()));
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":1000.00,\"deliveryNodeCode\":\"SYS_DC_001\","
+ "\"institutionIdentifier\":\"ORG_000000000000\","
+ "\"requestTimestamp\":\"20260730153218\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"机构标识尚未反馈到商业银行端")));
mvc.perform(post(BASE_PATH + "/applications/feedback")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.applicationStatus").value("FEEDBACKED"))
.andExpect(jsonPath("$.data.feedbackStatus").value(2))
.andExpect(jsonPath("$.data.feedbackStatusText").value("已反馈"));
mvc.perform(get(BASE_PATH + "/applications/commercial-bank")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.andExpect(jsonPath("$.data.statusText").value("已确认"))
.andExpect(jsonPath("$.data.applicationStatus").value("FEEDBACKED"))
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(org.hamcrest.Matchers.startsWith("ORG_")))
.andExpect(jsonPath("$.data.fullInstitutionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
String issuedOrg = jdbcTemplate.queryForObject(
"SELECT institution_identifier FROM institution_identifier_application WHERE id = ?",
String.class, applicationId);
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":1589.85,\"deliveryNodeCode\":\"SYS_DC_001\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730153218\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
Number currencyRequestId = jdbcTemplate.queryForObject(
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Number.class, issuedOrg);
java.util.Map<String, Object> storedRequest = jdbcTemplate.queryForMap(
"SELECT institution_identifier, amount, delivery_node_code, request_timestamp, "
+ "request_original_text, request_digest FROM currency_generation_request WHERE id = ?",
currencyRequestId.longValue());
assertThat(storedRequest.get("institution_identifier")).isEqualTo(issuedOrg);
assertThat(storedRequest.get("amount").toString()).isEqualTo("1589.85");
assertThat(storedRequest.get("delivery_node_code")).isEqualTo("SYS_DC_001");
assertThat(storedRequest.get("request_timestamp")).isEqualTo("20260730153218");
assertThat(storedRequest.get("request_original_text")).isEqualTo(
issuedOrg + "|1589.85|SYS_DC_001|20260730153218");
assertThat(storedRequest.get("request_digest").toString()).matches("[0-9A-F]{64}");
mvc.perform(get(BASE_PATH + "/currency-requests/page")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.institutionIdentifier")
.value(storedRequest.get("institution_identifier")))
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.deliveryNodeCode").value("SYS_DC_001"))
.andExpect(jsonPath("$.data.requestTimestamp")
.value(storedRequest.get("request_timestamp")))
.andExpect(jsonPath("$.data.requestOriginalText")
.value(storedRequest.get("request_original_text")))
.andExpect(jsonPath("$.data.requestDigest")
.value(storedRequest.get("request_digest")));
mvc.perform(post(BASE_PATH + "/currency-requests/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"amount\":2000.00,\"deliveryNodeCode\":\"SYS_DC_002\","
+ "\"institutionIdentifier\":\"" + issuedOrg + "\","
+ "\"requestTimestamp\":\"20260730160000\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1));
Number repeatedRequestId = jdbcTemplate.queryForObject(
"SELECT id FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Number.class, issuedOrg);
assertThat(repeatedRequestId.longValue()).isEqualTo(currencyRequestId.longValue());
Integer requestCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM currency_generation_request WHERE institution_identifier = ? AND deleted = FALSE",
Integer.class, issuedOrg);
assertThat(requestCount).isEqualTo(1);
mvc.perform(post(BASE_PATH + "/currency-requests/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\""
+ "0000000000000000000000000000000000000000000000000000000000000001"
+ "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"当前实训主体的商业银行第二私钥")))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"本次实训第1次错误")));
assertThat(onlyScoreDetail().getCompletionStatus()).isEqualTo("1");
mvc.perform(post(BASE_PATH + "/currency-requests/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(BASE_PATH + "/currency-requests/steps/package")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("\"requestId\":\"REQ_20260730_"),
org.hamcrest.Matchers.containsString("\"orgId\":\"" + issuedOrg + "\""),
org.hamcrest.Matchers.containsString("\"amount\":\"2000.00\""),
org.hamcrest.Matchers.containsString("\"systemId\":\"SYS_DC_002\""),
org.hamcrest.Matchers.containsString("\"timestamp\":\"20260730160000\""),
org.hamcrest.Matchers.containsString("\"digest\":"),
org.hamcrest.Matchers.containsString("\"signature\":"))))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.not(
org.hamcrest.Matchers.containsString("\"originalText\""))))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.not(
org.hamcrest.Matchers.containsString("\"keyId\""))));
jdbcTemplate.update("UPDATE currency_generation_request SET deleted = TRUE WHERE id = ?",
currencyRequestId.longValue());
mvc.perform(get(BASE_PATH + "/currency-requests/page")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
InstitutionIdentifierApplicationEntity feedbacked = applicationMapper.selectById(applicationId);
assertThat(feedbacked.getStatus()).isEqualTo("FEEDBACKED");
assertThat(feedbacked.getDigest()).matches("[0-9A-F]{64}");
assertThat(feedbacked.getBankSignature()).isNotBlank();
assertThat(feedbacked.getCentralBankSignature()).isNotBlank();
assertThat(feedbacked.getInstitutionIdentifier()).startsWith("ORG_");
StuModuleScoreDetailsEntity score = onlyScoreDetail();
assertThat(score.getCompletionStatus()).isEqualTo("1");
assertThat(score.getSchedule()).isEqualTo(11.11);
assertThat(score.getScoreProject()).isEqualTo(11.05);
List<InstitutionIdentifierOperationLogEntity> logs = operationLogMapper.selectList(null);
assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId))
.extracting(InstitutionIdentifierOperationLogEntity::getOperation)
.containsExactly("PREPARE", "DIGEST", "SIGN", "PACKAGE", "SEND", "DECRYPT_VERIFY", "CONFIRM", "FEEDBACK");
}
@Test
void centralBankDecryptionVerificationRejectsWrongPublicKeyAndKeepsPackagedStatus() throws Exception {
long applicationId = digest("BKCHCNBJ20104");
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/package")
.header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/send")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SUBMITTED"))
.andExpect(jsonPath("$.data.sentToCentralBank").value(true));
mvc.perform(post(BASE_PATH + "/applications/decryption-verification")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"publicKey\":\"" + centralBankPublicKey() + "\",\"digest\":\""
+ applicationMapper.selectById(applicationId).getDigest() + "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"解密验证必须选择当前主体备案的商业银行第二公钥")))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
InstitutionIdentifierApplicationEntity packaged = applicationMapper.selectById(applicationId);
assertThat(packaged.getStatus()).isEqualTo("SUBMITTED");
InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0);
assertThat(error.getErrorType()).isEqualTo("WRONG_PUBLIC_KEY");
assertThat(error.getOperationStep()).isEqualTo("DECRYPT_VERIFY");
assertThat(error.getProvidedKeyId()).isNull();
}
@Test
void decryptionVerificationRejectsWrongDigestAndUpdatesScoreDetail() throws Exception {
long applicationId = digest("BKCHCNBJ20105");
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/applications/decryption-verification")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"publicKey\":\"" + bankPublicKey()
+ "\",\"digest\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("摘要")))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
assertThat(applicationMapper.selectById(applicationId).getStatus()).isEqualTo("SUBMITTED");
InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0);
assertThat(error.getErrorType()).isEqualTo("WRONG_DIGEST");
assertThat(error.getOperationStep()).isEqualTo("DECRYPT_VERIFY");
StuModuleScoreDetailsEntity score = onlyScoreDetail();
assertThat(score.getCompletionStatus()).isEqualTo("1");
assertThat(score.getScoreProject()).isEqualTo(0.0);
}
@Test
void confirmationValidatesCentralBankPrivateKeyOnlyWhenProvided() throws Exception {
long applicationId = advanceToVerified("BKCHCNBJ20103");
mvc.perform(post(BASE_PATH + "/applications/confirmation")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"WRONG\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"签名确权必须使用中央银行第一私钥")))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
InstitutionIdentifierApplicationEntity verified = applicationMapper.selectById(applicationId);
assertThat(verified.getStatus()).isEqualTo("VERIFIED");
assertThat(verified.getScoringCriteria()).isEqualTo(1);
InstitutionIdentifierTrainingErrorEntity error = trainingErrorMapper.selectList(null).get(0);
assertThat(error.getErrorType()).isEqualTo("WRONG_PRIVATE_KEY");
assertThat(error.getProvidedKeyId()).isNull();
InstitutionSm2KeyEntity centralBankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.CENTRAL_FIRST_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(centralBankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/applications/confirmation")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1));
}
private long advanceToVerified(String bankCode) throws Exception {
long applicationId = digest(bankCode);
InstitutionSm2KeyEntity bankKey = keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new);
String privateKey = privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey());
mvc.perform(post(BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"privateKey\":\"" + privateKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/package")
.header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/send")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("SUBMITTED"))
.andExpect(jsonPath("$.data.sentToCentralBank").value(true));
mvc.perform(post(BASE_PATH + "/applications/decryption-verification")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"publicKey\":\"" + bankPublicKey() + "\",\"digest\":\""
+ applicationMapper.selectById(applicationId).getDigest() + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("VERIFIED"));
return applicationId;
}
private String bankPublicKey() {
return keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new).getPublicKey();
}
private String centralBankPublicKey() {
return keyMapper.selectList(null).stream()
.filter(key -> InstitutionKeyService.CENTRAL_FIRST_KEY.equals(key.getKeyId()))
.findFirst().orElseThrow(AssertionError::new).getPublicKey();
}
private StuModuleScoreDetailsEntity onlyScoreDetail() {
List<StuModuleScoreDetailsEntity> details = scoreDetailsMapper.selectList(null);
assertThat(details).hasSize(1);
return details.get(0);
}
private long digest(String bankCode) throws Exception {
mvc.perform(post(BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"bankCode\":\"" + bankCode + "|20260730143218\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.applicationId").doesNotExist())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.algorithm").doesNotExist());
return applicationMapper.selectList(null).stream()
.max(java.util.Comparator.comparing(InstitutionIdentifierApplicationEntity::getCreatedAt)
.thenComparing(InstitutionIdentifierApplicationEntity::getId))
.orElseThrow(() -> new AssertionError("机构标识申请记录未创建")).getId();
}
}