feat(mq-starter-idempotent-redis): mq.idempotent.* 中立配置 + 不按 mq.type 门控的去重 store 自动装配
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.njcn.mq.idempotent.redis;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link MqIdempotentProperties} 默认值守护:必须与迁移前 RedisStreamMqProperties.Idempotent
|
||||
* 完全一致(processing 60s / success 300s / fail 300s / 前缀 mq:idem:),保证迁移不改变现有行为。
|
||||
*/
|
||||
class MqIdempotentPropertiesTest {
|
||||
|
||||
@Test
|
||||
void defaults_matchLegacyRedisStreamIdempotentDefaults() {
|
||||
MqIdempotentProperties p = new MqIdempotentProperties();
|
||||
assertTrue(p.isEnabled(), "去重装配默认开启,仅显式 false 关闭");
|
||||
assertEquals("mq:idem:", p.getKeyPrefix());
|
||||
assertEquals(Duration.ofSeconds(60), p.getProcessingTtl());
|
||||
assertEquals(Duration.ofSeconds(300), p.getSuccessTtl());
|
||||
assertEquals(Duration.ofSeconds(300), p.getFailTtl());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.njcn.mq.idempotent.redis;
|
||||
|
||||
import com.njcn.mq.autoconfig.MqCoreAutoConfiguration;
|
||||
import com.njcn.mq.spi.MqIdempotentStore;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* 装配条件矩阵(ApplicationContextRunner,不连真实 Redis):
|
||||
* 关键契约是「不按 mq.type 门控、只按 Redis 在场门控」—— rocketmq 部署也能装出 store。
|
||||
*/
|
||||
class MqIdempotentRedisAutoConfigurationTest {
|
||||
|
||||
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(MqIdempotentRedisAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void withStringRedisTemplate_storeAutoConfigured_regardlessOfMqType() {
|
||||
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
|
||||
// 故意设 mq.type=rocketmq:装配必须与 mq.type 无关(这正是 rocketmq 用上去重的关键)
|
||||
.withPropertyValues("mq.type=rocketmq")
|
||||
.run(ctx -> {
|
||||
assertThat(ctx).hasSingleBean(MqIdempotentStore.class);
|
||||
assertThat(ctx.getBean(MqIdempotentStore.class)).isInstanceOf(RedisMqIdempotentStore.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutRedisBean_noStoreAndNoStartupFailure() {
|
||||
runner.run(ctx -> {
|
||||
assertThat(ctx).hasNotFailed();
|
||||
assertThat(ctx).doesNotHaveBean(MqIdempotentStore.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledFalse_disablesStore() {
|
||||
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
|
||||
.withPropertyValues("mq.idempotent.enabled=false")
|
||||
.run(ctx -> assertThat(ctx).doesNotHaveBean(MqIdempotentStore.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void userProvidedStore_backsOff() {
|
||||
MqIdempotentStore custom = mock(MqIdempotentStore.class);
|
||||
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
|
||||
.withBean("customStore", MqIdempotentStore.class, () -> custom)
|
||||
.run(ctx -> {
|
||||
assertThat(ctx).hasSingleBean(MqIdempotentStore.class);
|
||||
assertThat(ctx.getBean(MqIdempotentStore.class)).isSameAs(custom);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void declaresAutoConfigureOrderContract() {
|
||||
// 真实 Spring Boot 按类名字母序处理 autoconfig:com.njcn.mq.autoconfig(core) 先于
|
||||
// com.njcn.mq.idempotent(本模块)。显式声明顺序契约,守护 spec §4.4 的装配语义。
|
||||
AutoConfigureBefore before = MqIdempotentRedisAutoConfiguration.class.getAnnotation(AutoConfigureBefore.class);
|
||||
assertThat(before).as("必须声明 @AutoConfigureBefore(MqCoreAutoConfiguration)").isNotNull();
|
||||
assertThat(before.value()).contains(MqCoreAutoConfiguration.class);
|
||||
|
||||
AutoConfigureAfter after = MqIdempotentRedisAutoConfiguration.class.getAnnotation(AutoConfigureAfter.class);
|
||||
assertThat(after).as("必须声明 @AutoConfigureAfter(RedisAutoConfiguration),否则 @ConditionalOnBean(StringRedisTemplate) 求值时模板尚未注册").isNotNull();
|
||||
assertThat(after.value()).contains(RedisAutoConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredInSpringFactories() {
|
||||
List<String> names = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
|
||||
getClass().getClassLoader());
|
||||
assertThat(names).contains(MqIdempotentRedisAutoConfiguration.class.getName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user