|
|
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();
|
|
|
}
|
|
|
}
|