yeg
2026-09-17 ab9b1f30b848d1f763fd0012e7ec9e1900f7d019
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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);
        }
    }
}