feat: align exchange actions and freeze debit funds
parent
3a6cdae136
commit
06816b536c
@ -0,0 +1,234 @@
|
||||
package com.yau.digitalrmb.exchange.application.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yau.digitalrmb.exchange.application.command.CreateExchangeCommand;
|
||||
import com.yau.digitalrmb.exchange.application.query.ExchangeOrderView;
|
||||
import com.yau.digitalrmb.exchange.domain.model.ExchangeActor;
|
||||
import com.yau.digitalrmb.exchange.domain.model.ExchangeContext;
|
||||
import com.yau.digitalrmb.exchange.interfaces.dto.ExchangeActionRequest;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import com.yau.digitalrmb.training.attempt.application.ActionOutcome;
|
||||
import com.yau.digitalrmb.training.attempt.application.ExperimentAttemptService;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
|
||||
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView;
|
||||
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ExchangeTrainingActionService {
|
||||
private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
|
||||
.withZone(ZoneOffset.UTC);
|
||||
private static final List<String> ACTIONS = Arrays.asList(
|
||||
"01:refresh-wallet", "01:confirm-binding", "01:refresh-institution",
|
||||
"02:generate-timestamp", "02:concatenate-withdrawal", "02:output-withdrawal-request",
|
||||
"03:generate-sign-timestamp", "03:concatenate-sign-source", "03:generate-digest",
|
||||
"03:wallet-sign", "03:package-request", "03:send-request",
|
||||
"04:verify-wallet-signature", "04:check-account-balance", "04:check-wallet-limits",
|
||||
"04:execute-debit-hold", "04:output-debit-voucher");
|
||||
|
||||
private final ExperimentAttemptService attempts;
|
||||
private final ExchangeApplicationService exchanges;
|
||||
private final InstitutionIdentityCryptography cryptography;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ExchangeTrainingActionService(ExperimentAttemptService attempts,
|
||||
ExchangeApplicationService exchanges,
|
||||
InstitutionIdentityCryptography cryptography,
|
||||
ObjectMapper objectMapper) {
|
||||
this.attempts = attempts;
|
||||
this.exchanges = exchanges;
|
||||
this.cryptography = cryptography;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public ExperimentActionView<?> execute(UUID attemptId, String stepCode, String actionCode,
|
||||
ExchangeActionRequest request, ExperimentSubject subject,
|
||||
ExchangeActor actor) {
|
||||
String key = stepCode + ":" + actionCode;
|
||||
int index = ACTIONS.indexOf(key);
|
||||
if (index < 0) throw validation("不支持的兑换实验动作:" + key);
|
||||
ExchangeActionRequest input = request == null ? new ExchangeActionRequest() : request;
|
||||
ExperimentAttemptView attempt = attempts.detail(attemptId, subject);
|
||||
requirePrevious(attempt, index);
|
||||
UUID exchangeId = businessId(attempt.getBusinessId());
|
||||
if (index > ACTIONS.indexOf("02:output-withdrawal-request") && exchangeId == null) {
|
||||
throw validation("请先输出取币请求报文");
|
||||
}
|
||||
String[] next = ACTIONS.get(Math.min(index + 1, ACTIONS.size() - 1)).split(":", 2);
|
||||
final UUID boundExchangeId = exchangeId;
|
||||
return attempts.execute(attemptId, subject, stepCode, actionCode,
|
||||
fingerprint(key, input, subject), Object.class,
|
||||
() -> run(key, input, attempt, boundExchangeId, actor), next[0], next[1]);
|
||||
}
|
||||
|
||||
private ActionOutcome<Object> run(String key, ExchangeActionRequest request,
|
||||
ExperimentAttemptView attempt, UUID exchangeId,
|
||||
ExchangeActor actor) {
|
||||
switch (key) {
|
||||
case "01:refresh-wallet":
|
||||
return completed(exchanges.initializeContext(actor));
|
||||
case "01:confirm-binding":
|
||||
case "01:refresh-institution":
|
||||
return completed(exchanges.context(actor));
|
||||
case "02:generate-timestamp":
|
||||
case "03:generate-sign-timestamp":
|
||||
return completed(value("timestamp", TIMESTAMP.format(Instant.now())));
|
||||
case "02:concatenate-withdrawal":
|
||||
return completed(withdrawalOutput(request, attempt, actor));
|
||||
case "02:output-withdrawal-request": {
|
||||
Map<String, Object> source = prior(attempt, "02", "concatenate-withdrawal");
|
||||
ExchangeOrderView created = exchanges.create(new CreateExchangeCommand(
|
||||
text(source, "walletId"), text(source, "bankAccountId"),
|
||||
decimal(source, "amount"), text(source, "timestamp")), actor);
|
||||
return ActionOutcome.completed(created, created.getId());
|
||||
}
|
||||
case "03:concatenate-sign-source": {
|
||||
ExchangeOrderView order = exchanges.get(exchangeId, actor);
|
||||
ExchangeContext context = exchanges.context(actor);
|
||||
String timestamp = text(prior(attempt, "03", "generate-sign-timestamp"), "timestamp");
|
||||
String source = "SIGN|" + order.getRequestNo() + "|" + order.getAmount().toPlainString()
|
||||
+ "|" + context.getBankCardLast4() + "|" + timestamp;
|
||||
return completed(value("sourceText", source));
|
||||
}
|
||||
case "03:generate-digest": {
|
||||
String source = text(prior(attempt, "03", "concatenate-sign-source"), "sourceText");
|
||||
return completed(value("digest", cryptography.sm3(source)));
|
||||
}
|
||||
case "03:wallet-sign": {
|
||||
String source = text(prior(attempt, "03", "concatenate-sign-source"), "sourceText");
|
||||
String digest = text(prior(attempt, "03", "generate-digest"), "digest");
|
||||
return completed(exchanges.sign(exchangeId, source, digest, actor));
|
||||
}
|
||||
case "03:package-request":
|
||||
case "03:send-request":
|
||||
case "04:output-debit-voucher":
|
||||
return completed(exchanges.get(exchangeId, actor));
|
||||
case "04:verify-wallet-signature":
|
||||
return completed(exchanges.verifyWalletSignature(exchangeId, actor));
|
||||
case "04:check-account-balance":
|
||||
return completed(exchanges.checkAccountBalance(exchangeId, actor));
|
||||
case "04:check-wallet-limits":
|
||||
return completed(exchanges.checkWalletLimits(exchangeId, actor));
|
||||
case "04:execute-debit-hold":
|
||||
return completed(exchanges.bankProcess(exchangeId, actor));
|
||||
default:
|
||||
throw validation("不支持的兑换实验动作:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> withdrawalOutput(ExchangeActionRequest request,
|
||||
ExperimentAttemptView attempt,
|
||||
ExchangeActor actor) {
|
||||
ExchangeContext context = exchanges.context(actor);
|
||||
if (request.getWalletId() != null && !context.getWalletId().equals(request.getWalletId())) {
|
||||
throw validation("钱包不属于当前用户");
|
||||
}
|
||||
if (request.getBankAccountId() != null && !context.getBankAccountId().equals(request.getBankAccountId())) {
|
||||
throw validation("银行账户不属于当前用户");
|
||||
}
|
||||
BigDecimal amount = money(request.getAmount());
|
||||
String timestamp = text(prior(attempt, "02", "generate-timestamp"), "timestamp");
|
||||
Map<String, Object> output = new LinkedHashMap<String, Object>();
|
||||
output.put("walletId", context.getWalletId());
|
||||
output.put("bankAccountId", context.getBankAccountId());
|
||||
output.put("bankCardNumber", context.getBankCardNumber());
|
||||
output.put("amount", amount.toPlainString());
|
||||
output.put("timestamp", timestamp);
|
||||
output.put("sourceText", "WITHDRAW|" + context.getWalletId() + "|" + amount.toPlainString()
|
||||
+ "|" + context.getBankCardNumber() + "|" + timestamp);
|
||||
return output;
|
||||
}
|
||||
|
||||
private void requirePrevious(ExperimentAttemptView attempt, int index) {
|
||||
if (index == 0) return;
|
||||
String[] previous = ACTIONS.get(index - 1).split(":", 2);
|
||||
for (ExperimentActionView<?> action : attempt.getActions()) {
|
||||
if (previous[0].equals(action.getStepCode()) && previous[1].equals(action.getActionCode())
|
||||
&& action.getActionStatus() == ActionStatus.COMPLETED) return;
|
||||
}
|
||||
throw validation("请先完成动作:" + ACTIONS.get(index - 1));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> prior(ExperimentAttemptView attempt, String step, String actionCode) {
|
||||
for (ExperimentActionView<?> action : attempt.getActions()) {
|
||||
if (step.equals(action.getStepCode()) && actionCode.equals(action.getActionCode())
|
||||
&& action.getActionStatus() == ActionStatus.COMPLETED
|
||||
&& action.getOutput() instanceof Map) {
|
||||
return (Map<String, Object>) action.getOutput();
|
||||
}
|
||||
}
|
||||
throw validation("缺少前置动作结果:" + step + ":" + actionCode);
|
||||
}
|
||||
|
||||
private Map<String, Object> value(String name, Object value) {
|
||||
Map<String, Object> output = new LinkedHashMap<String, Object>();
|
||||
output.put(name, value);
|
||||
return output;
|
||||
}
|
||||
|
||||
private ActionOutcome<Object> completed(Object output) { return ActionOutcome.completed(output); }
|
||||
|
||||
private UUID businessId(String value) {
|
||||
if (value == null || value.trim().isEmpty()) return null;
|
||||
try { return UUID.fromString(value); }
|
||||
catch (IllegalArgumentException exception) {
|
||||
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "兑换实验绑定的业务标识无效");
|
||||
}
|
||||
}
|
||||
|
||||
private String text(Map<String, Object> values, String name) {
|
||||
Object value = values.get(name);
|
||||
if (value == null || value.toString().trim().isEmpty()) throw validation("缺少派生参数:" + name);
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private BigDecimal decimal(Map<String, Object> values, String name) {
|
||||
try { return new BigDecimal(text(values, name)); }
|
||||
catch (NumberFormatException exception) { throw validation("派生金额无效:" + name); }
|
||||
}
|
||||
|
||||
private BigDecimal money(BigDecimal value) {
|
||||
if (value == null || value.signum() <= 0 || value.scale() > 2) throw validation("兑换金额必须大于零且最多两位小数");
|
||||
return value.setScale(2);
|
||||
}
|
||||
|
||||
private String fingerprint(String key, ExchangeActionRequest request, ExperimentSubject subject) {
|
||||
Map<String, Object> values = new LinkedHashMap<String, Object>();
|
||||
values.put("action", key);
|
||||
values.put("walletId", request.getWalletId());
|
||||
values.put("bankAccountId", request.getBankAccountId());
|
||||
values.put("amount", request.getAmount());
|
||||
values.put("userId", subject.getUserId());
|
||||
try {
|
||||
byte[] json = objectMapper.writeValueAsString(values).getBytes(StandardCharsets.UTF_8);
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(json);
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte value : digest) result.append(String.format("%02x", value & 0xff));
|
||||
return result.toString();
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException exception) {
|
||||
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "无法生成动作幂等指纹");
|
||||
}
|
||||
}
|
||||
|
||||
private BusinessException validation(String message) {
|
||||
return new BusinessException(ErrorCode.VALIDATION_ERROR, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.yau.digitalrmb.exchange.domain.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class DebitHoldResult {
|
||||
private final BigDecimal beforeBalance;
|
||||
private final BigDecimal availableBalanceAfterHold;
|
||||
private final String debitVoucherNo;
|
||||
|
||||
public DebitHoldResult(BigDecimal beforeBalance, BigDecimal availableBalanceAfterHold,
|
||||
String debitVoucherNo) {
|
||||
this.beforeBalance = beforeBalance;
|
||||
this.availableBalanceAfterHold = availableBalanceAfterHold;
|
||||
this.debitVoucherNo = debitVoucherNo;
|
||||
}
|
||||
|
||||
public BigDecimal getBeforeBalance() { return beforeBalance; }
|
||||
public BigDecimal getAvailableBalanceAfterHold() { return availableBalanceAfterHold; }
|
||||
public String getDebitVoucherNo() { return debitVoucherNo; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.yau.digitalrmb.exchange.interfaces.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "兑换实验单步动作输入;时间戳、原文、摘要和签名均由服务端生成")
|
||||
public class ExchangeActionRequest {
|
||||
private String walletId;
|
||||
private String bankAccountId;
|
||||
private BigDecimal amount;
|
||||
|
||||
public String getWalletId() { return walletId; }
|
||||
public void setWalletId(String walletId) { this.walletId = walletId; }
|
||||
public String getBankAccountId() { return bankAccountId; }
|
||||
public void setBankAccountId(String bankAccountId) { this.bankAccountId = bankAccountId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package com.yau.digitalrmb.exchange.interfaces.rest;
|
||||
|
||||
import com.yau.digitalrmb.exchange.application.service.ExchangeTrainingActionService;
|
||||
import com.yau.digitalrmb.exchange.domain.model.ExchangeActor;
|
||||
import com.yau.digitalrmb.exchange.interfaces.dto.ExchangeActionRequest;
|
||||
import com.yau.digitalrmb.security.application.CurrentUser;
|
||||
import com.yau.digitalrmb.security.application.CurrentUserService;
|
||||
import com.yau.digitalrmb.shared.api.ApiResponse;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import com.yau.digitalrmb.shared.web.TraceIdFilter;
|
||||
import com.yau.digitalrmb.training.attempt.application.ExperimentAttemptService;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
|
||||
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView;
|
||||
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentAttemptView;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/exchange/attempts")
|
||||
@Tag(name = "数字货币兑换实验动作", description = "模块四可恢复实验及前端独立按钮动作")
|
||||
public class ExchangeTrainingAttemptController {
|
||||
private final ExperimentAttemptService attempts;
|
||||
private final ExchangeTrainingActionService actions;
|
||||
private final CurrentUserService users;
|
||||
|
||||
public ExchangeTrainingAttemptController(ExperimentAttemptService attempts,
|
||||
ExchangeTrainingActionService actions,
|
||||
CurrentUserService users) {
|
||||
this.attempts = attempts;
|
||||
this.actions = actions;
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "开始新的兑换实验")
|
||||
public ApiResponse<ExperimentAttemptView> create() {
|
||||
return ok(attempts.create(ExperimentModule.EXCHANGE, subject(users.getCurrentUser())));
|
||||
}
|
||||
|
||||
@GetMapping("/current")
|
||||
@Operation(summary = "恢复当前兑换实验")
|
||||
public ApiResponse<ExperimentAttemptView> current() {
|
||||
return ok(attempts.current(ExperimentModule.EXCHANGE, subject(users.getCurrentUser())));
|
||||
}
|
||||
|
||||
@GetMapping("/{attemptId}")
|
||||
@Operation(summary = "查询兑换实验详情和已保存动作")
|
||||
public ApiResponse<ExperimentAttemptView> detail(@PathVariable UUID attemptId) {
|
||||
return ok(attempts.detail(attemptId, subject(users.getCurrentUser())));
|
||||
}
|
||||
|
||||
@PostMapping("/{attemptId}/cancel")
|
||||
@Operation(summary = "取消兑换实验")
|
||||
public ApiResponse<ExperimentAttemptView> cancel(@PathVariable UUID attemptId) {
|
||||
return ok(attempts.cancel(attemptId, subject(users.getCurrentUser())));
|
||||
}
|
||||
|
||||
@PostMapping("/{attemptId}/steps/{stepCode}/actions/{actionCode}")
|
||||
@Operation(summary = "执行一个兑换实验按钮动作")
|
||||
public ApiResponse<ExperimentActionView<?>> execute(@PathVariable UUID attemptId,
|
||||
@PathVariable String stepCode,
|
||||
@PathVariable String actionCode,
|
||||
@RequestBody(required = false) ExchangeActionRequest request) {
|
||||
CurrentUser user = users.getCurrentUser();
|
||||
return ok(actions.execute(attemptId, stepCode, actionCode,
|
||||
request == null ? new ExchangeActionRequest() : request, subject(user), actor(user)));
|
||||
}
|
||||
|
||||
private ExperimentSubject subject(CurrentUser user) {
|
||||
return new ExperimentSubject(required(user.getUserId(), "用户 ID"),
|
||||
numeric(user.getSchoolId(), "学校 ID"), numeric(user.getClassId(), "班级 ID"), "");
|
||||
}
|
||||
|
||||
private ExchangeActor actor(CurrentUser user) {
|
||||
return new ExchangeActor(required(user.getUserId(), "用户 ID"), required(user.getName(), "用户姓名"),
|
||||
numeric(user.getSchoolId(), "学校 ID"), numeric(user.getClassId(), "班级 ID"));
|
||||
}
|
||||
|
||||
private long numeric(String value, String name) {
|
||||
try {
|
||||
long result = Long.parseLong(required(value, name));
|
||||
if (result <= 0) throw new NumberFormatException(name);
|
||||
return result;
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录凭据中的" + name + "无效");
|
||||
}
|
||||
}
|
||||
|
||||
private String required(String value, String name) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录凭据缺少" + name);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private <T> ApiResponse<T> ok(T data) {
|
||||
return ApiResponse.success(data, MDC.get(TraceIdFilter.MDC_KEY));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package com.yau.digitalrmb.exchange.interfaces.rest;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.yau.digitalrmb.exchange.application.service.ExchangeTrainingActionService;
|
||||
import com.yau.digitalrmb.security.application.CurrentUser;
|
||||
import com.yau.digitalrmb.security.application.CurrentUserService;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
|
||||
import com.yau.digitalrmb.training.attempt.domain.AttemptStatus;
|
||||
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
|
||||
import com.yau.digitalrmb.training.attempt.interfaces.dto.ExperimentActionView;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
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 ExchangeTrainingAttemptControllerTest {
|
||||
private static final String USER_ID = "00000000-0000-0000-0000-000000000488";
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
@MockBean private ExchangeTrainingActionService actionService;
|
||||
@MockBean private CurrentUserService currentUserService;
|
||||
|
||||
@Test
|
||||
void exposesLifecycleAndEveryExchangeActionThroughStepFour() throws Exception {
|
||||
when(currentUserService.getCurrentUser()).thenReturn(user());
|
||||
String created = mockMvc.perform(post("/api/v1/exchange/attempts").with(jwtForUser()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.moduleCode").value("EXCHANGE"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
UUID attemptId = UUID.fromString(JsonPath.read(created, "$.data.attemptId"));
|
||||
|
||||
when(actionService.execute(any(UUID.class), anyString(), anyString(), any(), any(), any()))
|
||||
.thenAnswer(invocation -> new ExperimentActionView<Object>(invocation.getArgument(0),
|
||||
ExperimentModule.EXCHANGE, invocation.getArgument(1), invocation.getArgument(2),
|
||||
ActionStatus.COMPLETED, AttemptStatus.IN_PROGRESS, invocation.getArgument(1),
|
||||
invocation.getArgument(2), Instant.now(), Collections.singletonMap("safe", true)));
|
||||
|
||||
String[][] actions = {
|
||||
{"01", "refresh-wallet"}, {"01", "confirm-binding"}, {"01", "refresh-institution"},
|
||||
{"02", "generate-timestamp"}, {"02", "concatenate-withdrawal"},
|
||||
{"02", "output-withdrawal-request"}, {"03", "generate-sign-timestamp"},
|
||||
{"03", "concatenate-sign-source"}, {"03", "generate-digest"},
|
||||
{"03", "wallet-sign"}, {"03", "package-request"}, {"03", "send-request"},
|
||||
{"04", "verify-wallet-signature"}, {"04", "check-account-balance"},
|
||||
{"04", "check-wallet-limits"}, {"04", "execute-debit-hold"},
|
||||
{"04", "output-debit-voucher"}
|
||||
};
|
||||
for (String[] action : actions) {
|
||||
mockMvc.perform(post("/api/v1/exchange/attempts/{id}/steps/{step}/actions/{action}",
|
||||
attemptId, action[0], action[1]).with(jwtForUser())
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.stepCode").value(action[0]))
|
||||
.andExpect(jsonPath("$.data.actionCode").value(action[1]));
|
||||
}
|
||||
|
||||
mockMvc.perform(get("/api/v1/exchange/attempts/current").with(jwtForUser()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString()));
|
||||
mockMvc.perform(get("/api/v1/exchange/attempts/{id}", attemptId).with(jwtForUser()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString()));
|
||||
mockMvc.perform(post("/api/v1/exchange/attempts/{id}/cancel", attemptId).with(jwtForUser()))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptStatus").value("CANCELLED"));
|
||||
}
|
||||
|
||||
private CurrentUser user() {
|
||||
return new CurrentUser("1001", "延安大学", null, null, null, null, 4L,
|
||||
USER_ID, "exchange", "兑换用户", "2001", "测试班", "exchange");
|
||||
}
|
||||
|
||||
private org.springframework.test.web.servlet.request.RequestPostProcessor jwtForUser() {
|
||||
return jwt().jwt(token -> token.subject(USER_ID).claim("userId", USER_ID));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue