fix(mq): B-04 显式 mq-enabled 默认关+最终凭据链+注册应用核验+动态 binding——平台 host 注入不再自动启用 MQ 消费
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# SH MQ 独立配置契约(2026-10-23)
|
||||
|
||||
- `svchall.fc.mq-enabled` 默认 false。配置类与消费者使用相同条件;平台注入 RabbitMQ host 不自动打开能力。
|
||||
- `svchall.fc.consumer-app-key` 默认引用最终 `yh.app-auth.app-key`,不再直接使用陈旧 env 回退 100018。
|
||||
- 只有显式启用且配置 host 才装配 MQ。启用时校验消费凭据键与签名键一致,再用已有 UserBase `getAppByCode(spring.application.name)` 核验注册行的 id/appKey/appCode 关系。
|
||||
- 业务应用主身份是 application_info.id;id 可以不等于凭据 appKey。禁止拿 id 或 appCode 替代 MQ 协议的 *AppKey 字段。
|
||||
- binding 继续动态生成 `fc.snapshot.v2.<自身消费凭据键>.*`,没有硬编码 100008。空键、通配符或额外 topic 段拒绝。
|
||||
- 仅默认链修正不会移除 broker 已有旧 binding;旧绑定、topic 权限及独立 vhost 清理必须由 zcode 在另行授权的环境验证。
|
||||
- 当前 MQ 投影入口的 DB 异常分支直接 basicNack(requeue=false),并吞掉异常;不能只凭工厂 retryTemplate 配置宣称发生了三次 listener 重试。实际死信路由和恢复能力 NOT RUN,本补丁未重构这一策略。
|
||||
- 本地 JSON 交接/ACK顺序测试不等于真实数据库提交或 broker 验证。核心 HTTP 签名收据链仍独立运行;本轮不处理 B5、不改 UserBase 模型、不启用对账。
|
||||
@@ -12,6 +12,8 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFacto
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import javax.annotation.PostConstruct;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -21,22 +23,22 @@ import org.springframework.retry.support.RetryTemplate;
|
||||
/**
|
||||
* v2 快照投递的消费侧拓扑(answer-delivery-v2 §3.3/§7,TASK_2 B.3/B.5)。
|
||||
*
|
||||
* <p><b>整体条件装配:只有显式配了 {@code spring.rabbitmq.host} 才存在</b>。未配 broker 时
|
||||
* <p><b>整体条件装配:只有显式设置 {@code svchall.fc.mq-enabled=true} 且配置 {@code spring.rabbitmq.host} 才存在</b>。未配 broker 时
|
||||
* 不建监听容器(否则后台反复重连把启动与日志拖脏),{@code FcAnswerConsumer} 上的
|
||||
* {@code @RabbitListener} 因没有处理器而不生效——可信建件仍走"签名收据确认"那条路,
|
||||
* 只是延迟从秒级变成"用户点确认那一刻"。</p>
|
||||
* {@code @RabbitListener} 因没有处理器而不生效。平台统一注入 host 不能自动打开消费能力;
|
||||
* 当前可信建件主链始终是签名收据 + HTTP,不依赖 MQ。</p>
|
||||
*
|
||||
* <p><b>队列隔离是这条链的硬前提</b>:完整值文档只进<b>按消费 appKey + 租户域绑定的私有队列</b>。
|
||||
* routing key 形如 {@code fc.snapshot.v2.<consumerAppKey>.<schoolId>},本应用只绑自己 appKey
|
||||
* 那一段({@code .*} 只覆盖自己的多租户域),<b>新消费者不得能绑其他目标的 routing key</b>
|
||||
* (v2 §5 撤权边界的 broker 侧一半)。部署必须落实的实值来源见交付说明"broker 隔离配置"一节:
|
||||
* 独立 vhost、账号仅对本 vhost 有 configure/write/read、禁止通配订阅他人前缀;
|
||||
* 这三条没有落地就不能打开 {@code RABBITMQ_HOST}——宁可不投,也不在共享 vhost 上发明文。</p>
|
||||
* 这三条没有落地就不能设置 {@code svchall.fc.mq-enabled=true}——宁可不投,也不在共享 vhost 上发明文。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@org.springframework.amqp.rabbit.annotation.EnableRabbit
|
||||
@ConditionalOnExpression("!'${spring.rabbitmq.host:}'.trim().isEmpty()")
|
||||
@ConditionalOnExpression("${svchall.fc.mq-enabled:false} && !'${spring.rabbitmq.host:}'.trim().isEmpty()")
|
||||
public class FcAnswerMqConfiguration {
|
||||
|
||||
/** 中心侧 v2 exchange(与 FC {@code AnswerDeliveryModels.EXCHANGE} 同一字面量) */
|
||||
@@ -49,9 +51,32 @@ public class FcAnswerMqConfiguration {
|
||||
public static final String DLX = "svc-hall.fc.answer.snapshot.v2.dlx";
|
||||
public static final String DLQ = QUEUE + ".dlq";
|
||||
|
||||
@Value("${svchall.fc.consumer-app-key:${yh.app-auth.app-key:100018}}")
|
||||
@Value("${svchall.fc.consumer-app-key:${yh.app-auth.app-key:}}")
|
||||
private String consumerAppKey;
|
||||
|
||||
@Value("${yh.app-auth.app-key:}")
|
||||
private String signingAppKey;
|
||||
|
||||
@Value("${spring.application.name:svc-hall}")
|
||||
private String applicationCode;
|
||||
|
||||
@Autowired
|
||||
private com.yunhai.platform.openapi.base.OpenApiAppClient appRegistryClient;
|
||||
|
||||
@PostConstruct
|
||||
public void validateIdentity() {
|
||||
String key = trimmedAppKey();
|
||||
if (signingAppKey == null || !key.equals(signingAppKey.trim())) {
|
||||
throw new IllegalStateException("CONSUMER_CREDENTIAL_MISMATCH");
|
||||
}
|
||||
com.fasterxml.jackson.databind.JsonNode app = appRegistryClient.getAppByCode(applicationCode);
|
||||
if (app == null || !app.path("id").isIntegralNumber() || !app.path("id").canConvertToLong()
|
||||
|| app.path("id").asLong() <= 0 || !key.equals(app.path("appKey").asText())
|
||||
|| !applicationCode.equals(app.path("appCode").asText())) {
|
||||
throw new IllegalStateException("CONSUMER_APPLICATION_MISMATCH");
|
||||
}
|
||||
}
|
||||
|
||||
@Value("${svchall.fc.prefetch-count:10}")
|
||||
private int prefetchCount;
|
||||
|
||||
@@ -123,6 +148,10 @@ public class FcAnswerMqConfiguration {
|
||||
}
|
||||
|
||||
private String trimmedAppKey() {
|
||||
return consumerAppKey == null ? "" : consumerAppKey.trim();
|
||||
String key = consumerAppKey == null ? "" : consumerAppKey.trim();
|
||||
if (!key.matches("[A-Za-z0-9_-]+")) {
|
||||
throw new IllegalStateException("INVALID_CONSUMER_ROUTE_KEY");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.List;
|
||||
/**
|
||||
* v2 快照消费者(answer-delivery-v2 §7,TASK_2 B.5/B.6)。
|
||||
*
|
||||
* <p>本类<b>整体条件装配</b>:只有显式配了 {@code spring.rabbitmq.host} 才存在(与
|
||||
* <p>本类<b>整体条件装配</b>:只有显式设置 {@code svchall.fc.mq-enabled=true} 且配置 {@code spring.rabbitmq.host} 才存在(与
|
||||
* {@link FcAnswerMqConfiguration} 同一条件表达式)。未接 MQ 的环境里 bean 不创建,
|
||||
* {@code @RabbitListener} 端点自然不被注册——"MQ 可选"必须落到启动期:注解处理器在
|
||||
* 容器工厂缺失时会直接抛 {@code NoSuchBeanDefinitionException} 打断启动,不会优雅跳过。
|
||||
@@ -49,7 +49,7 @@ import java.util.List;
|
||||
@Slf4j
|
||||
@Component
|
||||
@org.springframework.boot.autoconfigure.condition.ConditionalOnExpression(
|
||||
"!'${spring.rabbitmq.host:}'.trim().isEmpty()")
|
||||
"${svchall.fc.mq-enabled:false} && !'${spring.rabbitmq.host:}'.trim().isEmpty()")
|
||||
public class FcAnswerConsumer {
|
||||
|
||||
private final AnswerProjectionStore projectionStore;
|
||||
@@ -57,7 +57,7 @@ public class FcAnswerConsumer {
|
||||
private final FcAnswerInboxRepository inboxRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${svchall.fc.consumer-app-key:${yh.app-auth.app-key:100018}}")
|
||||
@Value("${svchall.fc.consumer-app-key:${yh.app-auth.app-key:}}")
|
||||
private String consumerAppKey;
|
||||
|
||||
@Value("${svchall.fc.report-batch-size:20}")
|
||||
|
||||
@@ -149,8 +149,10 @@ svchall:
|
||||
public-base-url: http://localhost:8086/upload/
|
||||
# ===== 可信建件与快照消费(answer-delivery-v2 §6~§8,TASK_2)=====
|
||||
fc:
|
||||
# 平台注入 RabbitMQ host 不代表 SH 允许消费;HTTP 收据建件主链不依赖 MQ。
|
||||
mq-enabled: ${SVCHALL_FC_MQ_ENABLED:false}
|
||||
# 本应用消费身份(默认取签名身份同一把 appKey;信封目标必须等于它,否则整封拒收)
|
||||
consumer-app-key: ${YH_APP_AUTH_APP_KEY:100018}
|
||||
consumer-app-key: ${yh.app-auth.app-key:}
|
||||
# 被嵌填报页基址 = 表单中心<b>前端</b>地址(不是后端 API 地址):iframe 的 src 由服务端拼,
|
||||
# 浏览器传进来的任何路径都不采信(A.2 末句)。缺键即拒开托管填报页——宁可不给入口,
|
||||
# 也不给一个"拼了个大概能打开的地址"的入口。本地=FC front dev(8089),生产 env 注入。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import com.yunhai.svchall.entity.FcAnswerInbox;
|
||||
import com.yunhai.svchall.model.FcApplyModels.SnapshotEnvelope;
|
||||
import com.yunhai.svchall.repository.FcAnswerInboxRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** Local JSON handoff and manual ACK/NACK ordering; not real broker or transaction commit evidence. */
|
||||
class FcAnswerConsumerMqTest {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private AnswerProjectionStore store;
|
||||
private FcSubmissionClient client;
|
||||
private FcAnswerInboxRepository rows;
|
||||
private FcAnswerConsumer consumer;
|
||||
private Channel channel;
|
||||
private final String json = "{\"protocolVersion\":\"fc.answer.snapshot.v2\",\"eventId\":\"evt\","
|
||||
+ "\"eventType\":\"updated\",\"sourceAppKey\":\"100025\",\"consumerAppKey\":\"100018\","
|
||||
+ "\"consumerSchoolId\":27,\"grantVersion\":4,\"formUuid\":\"form\",\"formVersion\":1,"
|
||||
+ "\"answerUuid\":\"answer\",\"answerRevision\":8,\"isDeleted\":false,\"contentHash\":\"hash\","
|
||||
+ "\"occurredAt\":\"2026-10-23T10:00:00+08:00\",\"snapshot\":{\"schId\":27,\"appCode\":\"owner\","
|
||||
+ "\"code\":\"leave\",\"submitterId\":41,\"subjectId\":42,\"status\":\"processed\","
|
||||
+ "\"values\":{\"reason\":\"leave\"},\"answerNo\":\"LEAVE-8\",\"createTime\":\"2026-10-23T10:00:00\","
|
||||
+ "\"updateTime\":\"2026-10-23T10:01:00\",\"intent\":{\"intentId\":\"intent\"}}}";
|
||||
|
||||
@BeforeEach void setUp() {
|
||||
store = mock(AnswerProjectionStore.class); client = mock(FcSubmissionClient.class);
|
||||
rows = mock(FcAnswerInboxRepository.class); channel = mock(Channel.class);
|
||||
consumer = new FcAnswerConsumer(store, client, rows, mapper);
|
||||
ReflectionTestUtils.setField(consumer, "consumerAppKey", "100018");
|
||||
}
|
||||
|
||||
@Test void validJsonPreservesEveryAnswerCoordinateBeforeAck() throws Exception {
|
||||
when(store.applySnapshot(any())).thenReturn(new AnswerProjectionStore.Disposition(
|
||||
AnswerProjectionStore.Outcome.REJECTED, "evt", FcAnswerInbox.STATE_REJECTED, 8L, "hash", null, null));
|
||||
consumer.onMessage(message(json), channel, 5L);
|
||||
ArgumentCaptor<SnapshotEnvelope> envelope = ArgumentCaptor.forClass(SnapshotEnvelope.class);
|
||||
InOrder order = inOrder(store, channel); order.verify(store).applySnapshot(envelope.capture());
|
||||
order.verify(channel).basicAck(5L, false);
|
||||
// Compare the complete JSON wire representation; DTO LongNode vs parsed IntNode is not data loss.
|
||||
com.fasterxml.jackson.databind.JsonNode decoded = mapper.readTree(mapper.writeValueAsBytes(envelope.getValue()));
|
||||
assertThat(decoded).isEqualTo(mapper.readTree(json));
|
||||
verifyNoInteractions(client);
|
||||
}
|
||||
|
||||
@Test void invalidJsonIsAckedAsPoisonWithoutPretendingItWasApplied() throws Exception {
|
||||
consumer.onMessage(message("{broken"), channel, 6L);
|
||||
verify(channel).basicAck(6L, false); verify(channel, never()).basicNack(anyLong(), anyBoolean(), anyBoolean());
|
||||
verifyNoInteractions(store, client);
|
||||
}
|
||||
|
||||
@Test void failedProjectionIsNackedWithoutAckOrFalseAppliedReport() throws Exception {
|
||||
when(store.applySnapshot(any())).thenThrow(new IllegalStateException("database unavailable"));
|
||||
consumer.onMessage(message(json), channel, 7L);
|
||||
verify(channel).basicNack(7L, false, false); verify(channel, never()).basicAck(anyLong(), anyBoolean());
|
||||
verifyNoInteractions(client);
|
||||
}
|
||||
|
||||
@Test void reportFailureKeepsTheCommittedOutcomeAndSchedulesOnlyReportRetry() throws Exception {
|
||||
when(store.applySnapshot(any())).thenReturn(new AnswerProjectionStore.Disposition(
|
||||
AnswerProjectionStore.Outcome.APPLIED, "evt", FcAnswerInbox.STATE_APPLIED, 8L, "hash", 42, null));
|
||||
FcAnswerInbox row = new FcAnswerInbox(); row.setState(FcAnswerInbox.STATE_APPLIED);
|
||||
when(rows.findByEventIdAndConsumerAppKey("evt", "100018")).thenReturn(Optional.of(row));
|
||||
consumer.onMessage(message(json), channel, 8L);
|
||||
InOrder order = inOrder(store, channel, client);
|
||||
order.verify(store).applySnapshot(any()); order.verify(channel).basicAck(8L, false);
|
||||
order.verify(client).reportApplied(any());
|
||||
assertThat(row.getState()).isEqualTo(FcAnswerInbox.STATE_APPLIED);
|
||||
assertThat(row.getReportState()).isEqualTo(FcAnswerInbox.REPORT_FAILED);
|
||||
assertThat(row.getReportAttempts()).isEqualTo(1);
|
||||
assertThat(row.getNextReportAt()).isAfter(LocalDateTime.now());
|
||||
verify(channel, never()).basicNack(anyLong(), anyBoolean(), anyBoolean());
|
||||
}
|
||||
|
||||
private Message message(String body) {
|
||||
MessageProperties props = new MessageProperties(); props.setContentType(MessageProperties.CONTENT_TYPE_JSON);
|
||||
return new Message(body.getBytes(StandardCharsets.UTF_8), props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.yunhai.svchall.config.FcAnswerMqConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** B-04 默认关闭的 Spring 条件装配回归,不启动真实 broker。 */
|
||||
class ShMqExplicitEnableTest {
|
||||
private final ApplicationContextRunner context = new ApplicationContextRunner()
|
||||
.withUserConfiguration(FcAnswerMqConfiguration.class, FcAnswerConsumer.class);
|
||||
|
||||
@Test
|
||||
void platformHostInjectionAloneDoesNotCreateTopologyOrConsumer() {
|
||||
context.withPropertyValues("spring.rabbitmq.host=platform-broker").run(ctx -> {
|
||||
assertThat(ctx).hasNotFailed();
|
||||
assertThat(ctx).doesNotHaveBean(FcAnswerConsumer.class);
|
||||
assertThat(ctx).doesNotHaveBean(FcAnswerMqConfiguration.class);
|
||||
assertThat(ctx).doesNotHaveBean("fcAnswerContainerFactory");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitFalseStillWinsWithPlatformHost() {
|
||||
context.withPropertyValues("spring.rabbitmq.host=platform-broker", "svchall.fc.mq-enabled=false")
|
||||
.run(ctx -> assertThat(ctx).hasNotFailed().doesNotHaveBean(FcAnswerConsumer.class)
|
||||
.doesNotHaveBean(FcAnswerMqConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enablingWithoutBrokerDoesNotCreateConsumerOrFactory() {
|
||||
context.withPropertyValues("svchall.fc.mq-enabled=true", "spring.rabbitmq.host=")
|
||||
.run(ctx -> assertThat(ctx).hasNotFailed().doesNotHaveBean(FcAnswerConsumer.class)
|
||||
.doesNotHaveBean("fcAnswerContainerFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bothProductionConditionsRequireTheSameExplicitTrueAndHost() {
|
||||
String topology = FcAnswerMqConfiguration.class.getAnnotation(ConditionalOnExpression.class).value();
|
||||
String consumer = FcAnswerConsumer.class.getAnnotation(ConditionalOnExpression.class).value();
|
||||
assertThat(consumer).isEqualTo(topology).contains("svchall.fc.mq-enabled:false");
|
||||
MockEnvironment env = new MockEnvironment().withProperty("svchall.fc.mq-enabled", "true")
|
||||
.withProperty("spring.rabbitmq.host", "broker");
|
||||
assertThat(new SpelExpressionParser().parseExpression(env.resolvePlaceholders(topology))
|
||||
.getValue(Boolean.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindingRemainsSnapshotPrefixForOwnConsumerIdentity() {
|
||||
FcAnswerMqConfiguration config = new FcAnswerMqConfiguration();
|
||||
ReflectionTestUtils.setField(config, "consumerAppKey", "100018");
|
||||
assertThat(config.fcSnapshotBinding().getRoutingKey()).isEqualTo("fc.snapshot.v2.100018.*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||
import com.yunhai.svchall.config.FcAnswerMqConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** Binding object and registry/config contract checks; actual broker routing is NOT RUN. */
|
||||
class ShMqRoutingIdentityTest {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test void yamlUsesTheFinalSigningPropertyInsteadOfTheStaleEnvironmentFallback() throws Exception {
|
||||
StandardEnvironment env = new StandardEnvironment();
|
||||
new YamlPropertySourceLoader().load("application", new ClassPathResource("application.yml"))
|
||||
.forEach(env.getPropertySources()::addLast);
|
||||
env.getPropertySources().addFirst(new MapPropertySource("final-platform-key",
|
||||
Collections.singletonMap("yh.app-auth.app-key", "100008")));
|
||||
assertThat(env.getProperty("svchall.fc.consumer-app-key")).isEqualTo("100008");
|
||||
assertThat(env.getProperty("svchall.fc.mq-enabled")).isEqualTo("false");
|
||||
}
|
||||
|
||||
@Test void matchingCredentialBuildsTheCanonical100018BindingWithoutUsingThePrimaryId() {
|
||||
FcAnswerMqConfiguration config = config("100018", "100018", registered(700018, 100018, "svc-hall"));
|
||||
assertThatCode(config::validateIdentity).doesNotThrowAnyException();
|
||||
assertThat(config.fcSnapshotBinding().getRoutingKey()).isEqualTo("fc.snapshot.v2.100018.*");
|
||||
assertThat(config.fcSnapshotBinding().getExchange()).isEqualTo("fc.answer.delivery.v2");
|
||||
}
|
||||
|
||||
@Test void stale100018CannotSurviveA100008FinalSigningConfiguration() {
|
||||
FcAnswerMqConfiguration config = config("100018", "100008", registered(700008, 100008, "svc-hall"));
|
||||
assertThatThrownBy(config::validateIdentity).hasMessageContaining("CONSUMER_CREDENTIAL_MISMATCH");
|
||||
}
|
||||
|
||||
@Test void configuredCredentialDeterminesBindingWithoutHardcoding100008() {
|
||||
FcAnswerMqConfiguration config = config("100008", "100008", registered(700008, 100008, "svc-hall"));
|
||||
config.validateIdentity();
|
||||
assertThat(config.fcSnapshotBinding().getRoutingKey()).isEqualTo("fc.snapshot.v2.100008.*")
|
||||
.isNotEqualTo("fc.snapshot.v2.100018.*");
|
||||
}
|
||||
|
||||
@Test void aCredentialRegisteredToAnotherBusinessApplicationIsRejected() {
|
||||
assertThatThrownBy(config("100018", "100018", registered(700025, 100025, "form-center"))::validateIdentity)
|
||||
.hasMessageContaining("CONSUMER_APPLICATION_MISMATCH");
|
||||
}
|
||||
|
||||
@Test void businessPrimaryIdIsNeverComparedEqualToOrSubstitutedForTheCredential() {
|
||||
assertThatThrownBy(config("700018", "700018", registered(700018, 100018, "svc-hall"))::validateIdentity)
|
||||
.hasMessageContaining("CONSUMER_APPLICATION_MISMATCH");
|
||||
}
|
||||
|
||||
@Test void emptyRegistryOrMissingPrimaryIdFailsClosedOnlyWhenMqIsExplicitlyEnabled() {
|
||||
assertThatThrownBy(config("100018", "100018", null)::validateIdentity)
|
||||
.hasMessageContaining("CONSUMER_APPLICATION_MISMATCH");
|
||||
ObjectNode incomplete = registered(700018, 100018, "svc-hall"); incomplete.remove("id");
|
||||
assertThatThrownBy(config("100018", "100018", incomplete)::validateIdentity)
|
||||
.hasMessageContaining("CONSUMER_APPLICATION_MISMATCH");
|
||||
}
|
||||
|
||||
@Test void routeWildcardsOrExtraWordsCannotExpandTheConsumersBinding() {
|
||||
for (String key : new String[]{"", "100018.*", "100018.#", "100018.27"}) {
|
||||
FcAnswerMqConfiguration config = config(key, key, null);
|
||||
assertThatThrownBy(config::fcSnapshotBinding).hasMessageContaining("INVALID_CONSUMER_ROUTE_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode registered(int id, int credential, String code) {
|
||||
return mapper.createObjectNode().put("id", id).put("appKey", credential).put("appCode", code);
|
||||
}
|
||||
|
||||
private FcAnswerMqConfiguration config(String consumer, String signing, ObjectNode application) {
|
||||
FcAnswerMqConfiguration config = new FcAnswerMqConfiguration();
|
||||
ReflectionTestUtils.setField(config, "consumerAppKey", consumer);
|
||||
ReflectionTestUtils.setField(config, "signingAppKey", signing);
|
||||
ReflectionTestUtils.setField(config, "applicationCode", "svc-hall");
|
||||
OpenApiAppClient registry = mock(OpenApiAppClient.class);
|
||||
when(registry.getAppByCode("svc-hall")).thenReturn(application);
|
||||
ReflectionTestUtils.setField(config, "appRegistryClient", registry);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user