package com.talentcard.server.business.payment;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import java.lang.reflect.Field;
|
import java.nio.charset.StandardCharsets;
|
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.test.context.ActiveProfiles;
|
|
/**
|
* 定时任务的开关与间隔必须能从配置读、且默认值一致。
|
*
|
* <p>为什么单独测这个:定时对账是支付与退款唯一的后台兜底,而它原先「注解默认关、配置里打开」——
|
* 一次配置编辑删掉那一行,兜底就**静默消失**;交易确认补偿的开关更是只写在注释里、配置键从未被读取。
|
* 这些都不会被任何业务用例发现,所以直接对着代码与配置的形状断言。
|
*/
|
@SpringBootTest
|
@ActiveProfiles("test")
|
class PaymentSchedulingConfigurationTests {
|
|
@Test
|
void everyPaymentBackgroundTaskCanBeReadFromTheConfigurationFile() throws Exception {
|
String yml = configurationFile();
|
assertThat(yml)
|
.as("对账间隔要写进配置,不能只靠注解默认值")
|
.contains("fixed-delay-ms: ${HUIFU_RECONCILIATION_FIXED_DELAY_MS:300000}");
|
assertThat(yml)
|
.as("交易确认补偿的开关要真的存在且可读")
|
.contains("enabled: ${FUND_CONFIRMATION_RETRY_ENABLED:true}");
|
assertThat(yml)
|
.as("悬账自动退款的开关要真的存在且可读")
|
.contains("enabled: ${STRANDED_REFUND_ENABLED:true}");
|
assertThat(yml)
|
.as("定时对账的开关默认与代码一致(删掉配置行不会让兜底静默消失)")
|
.contains("enabled: ${HUIFU_RECONCILIATION_ENABLED:true}");
|
}
|
|
@Test
|
void theReconciliationSwitchDefaultsToOnInTheCodeAsWell() throws Exception {
|
assertThat(defaultExpression(PaymentReconciliationScheduler.class, "enabled"))
|
.as("注解上的默认值必须与配置一致:默认关 + 配置打开,就等于一行配置编辑能关掉兜底")
|
.isEqualTo("${app.payment.huifu.reconciliation.enabled:true}");
|
assertThat(PaymentReconciliationScheduler.class.getMethod("reconcilePendingTransactions")
|
.getAnnotation(Scheduled.class).fixedDelayString())
|
.isEqualTo("${app.payment.huifu.reconciliation.fixed-delay-ms:300000}");
|
}
|
|
@Test
|
void theFundConfirmationRetrySwitchIsImplementedAndDefaultsToOn() throws Exception {
|
assertThat(defaultExpression(PaymentFundConfirmationRetry.class, "enabled"))
|
.as("注释里宣称有这个开关,代码就必须真的读它")
|
.isEqualTo("${app.payment.fund-confirmation-retry.enabled:true}");
|
}
|
|
@Test
|
void everyHuifuCallbackUrlIsReportedAndBlankOnesAreCalledOut() {
|
HuifuPaymentProperties properties = new HuifuPaymentProperties();
|
properties.setNotifyUrl("https://example.com/api/payments/huifu/notify");
|
HuifuCallbackUrlAudit audit = new HuifuCallbackUrlAudit(properties);
|
|
assertThat(audit.report())
|
.as("五个回调端点都要有结论(地址或「未启用」),而不是静默地什么都不说")
|
.containsOnlyKeys("notify", "merchant-business-notify", "merchant-onboarding-notify",
|
"merchant-wechat-config-notify", "merchant-business-open-notify")
|
.allSatisfy((endpoint, url) -> assertThat(url).isNotBlank());
|
|
properties.setNotifyUrl("");
|
assertThat(audit.report())
|
.as("任何一个回调地址为空时都必须报「回调未启用」,而不是留一个不可达的假地址")
|
.allSatisfy((endpoint, url) -> assertThat(url).isEmpty());
|
}
|
|
private String defaultExpression(Class<?> type, String fieldName) throws Exception {
|
Field field = type.getDeclaredField(fieldName);
|
return field.getAnnotation(Value.class).value();
|
}
|
|
private String configurationFile() throws Exception {
|
try (var stream = getClass().getResourceAsStream("/application.yml")) {
|
assertThat(stream).as("application.yml 必须在类路径上").isNotNull();
|
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
}
|
}
|
}
|