From 24507e01991bc8e12069be5d28cc2f29d1fed436 Mon Sep 17 00:00:00 2001 From: Pascal Dal Farra Date: Wed, 5 Jun 2024 11:36:48 +0200 Subject: [PATCH] test(amqp): cleanup test(amqp): update e2e test(amqp): wait for ready amqp server chore(amqp): add spring-messaging dependency in example test(amqp): persist patched asyncapi.yaml chore(amqp): use non-exclusive queue in example test: add WaitStrategy for ApiSystemTest feat(ui): show channel bindings test(amqp): update asyncapi artifacts chore(ui): fix formatting test(ui): use valid mock data test(amqp): update asyncapi artifacts Part of GH-366 feat(amqp): scan all queues In addition to the routingKeys chore: update asyncapi.yaml gradle script chore(amqp): simplify local testing chore(amqp): fix queue configuration tests: additional RabbitListener tests tests: refactoring + add yaml endpoint testing --- build.gradle | 3 + .../SpringwolfInitApplicationListener.java | 4 +- .../e2e/tests/publishing.spec.ts | 2 + .../springwolf-amqp-example/build.gradle | 1 + .../docker-compose.yml | 2 + .../examples/amqp/AmqpConstants.java | 41 ++ .../configuration/RabbitConfiguration.java | 18 +- .../amqp/consumers/ExampleConsumer.java | 97 ++++- .../examples/amqp/dtos/GenericPayloadDto.java | 19 + .../amqp/producers/AnotherProducer.java | 8 +- .../src/main/resources/application.properties | 2 +- .../examples/amqp/ApiIntegrationTest.java | 37 +- .../examples/amqp/ApiSystemTest.java | 2 + .../examples/amqp/ProducerSystemTest.java | 4 +- .../amqp/SpringContextIntegrationTest.java | 2 +- .../src/test/resources/asyncapi.json | 231 ++++++++++ .../src/test/resources/asyncapi.yaml | 406 ++++++++++++++++++ .../examples/cloudstream/ApiSystemTest.java | 2 + .../examples/jms/ApiSystemTest.java | 2 + .../examples/kafka/ApiSystemTest.java | 2 + .../examples/sns/ApiSystemTest.java | 2 + .../examples/sqs/ApiSystemTest.java | 2 + .../examples/stomp/stomp/ApiSystemTest.java | 2 + .../scanners/bindings/RabbitListenerUtil.java | 27 +- .../channels/RabbitQueueBeanScanner.java | 30 ++ .../SpringwolfAmqpScannerConfiguration.java | 11 + .../channels/RabbitQueueBeanScannerTest.java | 47 ++ .../channel-main.component.spec.ts | 4 +- .../channel-operation.component.spec.ts | 4 +- .../new/channels/channels.component.html | 16 + .../new/channels/channels.component.ts | 12 +- .../asyncapi/asyncapi-mapper.service.ts | 2 +- .../service/asyncapi/models/channels.model.ts | 2 +- .../app/service/mock/mock-asyncapi.service.ts | 2 +- 34 files changed, 1007 insertions(+), 41 deletions(-) create mode 100644 springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/AmqpConstants.java create mode 100644 springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/dtos/GenericPayloadDto.java create mode 100644 springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml create mode 100644 springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java create mode 100644 springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java diff --git a/build.gradle b/build.gradle index 06044eaf4..c95af39a1 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,9 @@ allprojects { project .file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.json') .renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.json') + project + .file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.yaml') + .renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.yaml') } } } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/SpringwolfInitApplicationListener.java b/springwolf-core/src/main/java/io/github/springwolf/core/SpringwolfInitApplicationListener.java index 952bf8f71..db68ab950 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/SpringwolfInitApplicationListener.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/SpringwolfInitApplicationListener.java @@ -24,10 +24,10 @@ public class SpringwolfInitApplicationListener implements ApplicationListener + * Note:
+ * The 'Default exchange' is a 'Direct exchange' with an empty name ( name= "") + */ + public static final String EXCHANGE_DEFAULT_EXCHANGE = ""; + + // Routing keys + /** + * When a queue is bound with "#" (hash) binding key, + * it will receive all the messages, regardless of the routing key - like in fanout exchange. + */ + public static final String ROUTING_KEY_ALL_MESSAGES = "#"; + + public static final String ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY = "example-topic-routing-key"; + + // Queues + public static final String QUEUE_EXAMPLE_QUEUE = "example-queue"; + public static final String QUEUE_ANOTHER_QUEUE = "another-queue"; + public static final String QUEUE_MULTI_PAYLOAD_QUEUE = "multi-payload-queue"; + public static final String QUEUE_EXAMPLE_BINDINGS_QUEUE = "example-bindings-queue"; + + public static final String QUEUE_CREATE = "queue-create"; + public static final String QUEUE_READ = "queue-read"; + public static final String QUEUE_DELETE = "queue-delete"; + public static final String QUEUE_UPDATE = "queue-update"; +} diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java index 2e92ce5e8..adfe68e26 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.examples.amqp.configuration; +import io.github.springwolf.examples.amqp.AmqpConstants; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Exchange; @@ -23,34 +24,39 @@ public Jackson2JsonMessageConverter converter() { @Bean public Queue exampleQueue() { - return new Queue("example-queue", false); + return new Queue(AmqpConstants.QUEUE_EXAMPLE_QUEUE, false); } @Bean public Queue anotherQueue() { - return new Queue("another-queue", false); + return new Queue(AmqpConstants.QUEUE_ANOTHER_QUEUE, false); } @Bean public Queue exampleBindingsQueue() { - return new Queue("example-bindings-queue", false, true, true); + return new Queue(AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, false, false, true); + } + + @Bean + public Queue queueRead() { + return new Queue(AmqpConstants.QUEUE_READ, false); } @Bean public Exchange exampleTopicExchange() { - return new TopicExchange("example-topic-exchange"); + return new TopicExchange(AmqpConstants.EXCHANGE_EXAMPLE_TOPIC_EXCHANGE); } @Bean public Queue multiPayloadQueue() { - return new Queue("multi-payload-queue"); + return new Queue(AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE); } @Bean public Binding exampleTopicBinding(Queue exampleBindingsQueue, Exchange exampleTopicExchange) { return BindingBuilder.bind(exampleBindingsQueue) .to(exampleTopicExchange) - .with("example-topic-routing-key") + .with(AmqpConstants.ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY) .noargs(); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java index fddf77bc0..79416adbc 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java @@ -1,16 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.examples.amqp.consumers; +import io.github.springwolf.examples.amqp.AmqpConstants; import io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto; import io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto; +import io.github.springwolf.examples.amqp.dtos.GenericPayloadDto; import io.github.springwolf.examples.amqp.producers.AnotherProducer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ExchangeTypes; +import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; @Component @@ -20,9 +24,9 @@ public class ExampleConsumer { private final AnotherProducer anotherProducer; - @RabbitListener(queues = "example-queue") + @RabbitListener(queues = AmqpConstants.QUEUE_EXAMPLE_QUEUE) public void receiveExamplePayload(ExamplePayloadDto payload) { - log.info("Received new message in example-queue: {}", payload.toString()); + log.info("Received new message in {}: {}", AmqpConstants.QUEUE_EXAMPLE_QUEUE, payload.toString()); AnotherPayloadDto example = new AnotherPayloadDto(); example.setExample(payload); @@ -31,37 +35,102 @@ public void receiveExamplePayload(ExamplePayloadDto payload) { anotherProducer.sendMessage(example); } - @RabbitListener(queues = "another-queue") + @RabbitListener(queues = AmqpConstants.QUEUE_ANOTHER_QUEUE) public void receiveAnotherPayload(AnotherPayloadDto payload) { - log.info("Received new message in another-queue: {}", payload.toString()); + log.info("Received new message in {}: {}", AmqpConstants.QUEUE_ANOTHER_QUEUE, payload.toString()); } @RabbitListener( bindings = { @QueueBinding( - exchange = @Exchange(name = "example-topic-exchange", type = ExchangeTypes.TOPIC), + exchange = + @Exchange( + name = AmqpConstants.EXCHANGE_EXAMPLE_TOPIC_EXCHANGE, + type = ExchangeTypes.TOPIC), value = @Queue( - name = "example-bindings-queue", + name = AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, durable = "false", - exclusive = "true", + exclusive = "false", autoDelete = "true"), - key = "example-topic-routing-key") + key = AmqpConstants.ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY) }) public void bindingsExample(AnotherPayloadDto payload) { log.info( - "Received new message in example-bindings-queue" - + " through exchange example-topic-exchange using routing key example-topic-routing-key: {}", + "Received new message in {}" + " through exchange {}" + " using routing key {}: {}", + AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, + AmqpConstants.EXCHANGE_EXAMPLE_TOPIC_EXCHANGE, + AmqpConstants.ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY, payload.toString()); } - @RabbitListener(queues = "multi-payload-queue") + @RabbitListener(queues = AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE) public void bindingsBeanExample(AnotherPayloadDto payload) { - log.info("Received new message in multi-payload-queue (AnotherPayloadDto): {}", payload.toString()); + log.info( + "Received new message in {} (AnotherPayloadDto): {}", + AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE, + payload.toString()); } - @RabbitListener(queues = "multi-payload-queue") + @RabbitListener(queues = AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE) public void bindingsBeanExample(ExamplePayloadDto payload) { - log.info("Received new message in multi-payload-queue (ExamplePayloadDto): {}", payload.toString()); + log.info( + "Received new message in {} (ExamplePayloadDto): {}", + AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE, + payload.toString()); + } + + @RabbitListener(queuesToDeclare = @Queue(name = AmqpConstants.QUEUE_CREATE, autoDelete = "false", durable = "true")) + public void queuesToDeclareCreate(Message message, @Payload GenericPayloadDto payload) { + log.info( + "Received new message {} in {} (GenericPayloadDto): {}", + message, + AmqpConstants.QUEUE_CREATE, + payload.toString()); + } + + @RabbitListener(queuesToDeclare = @Queue(name = AmqpConstants.QUEUE_DELETE, autoDelete = "false", durable = "true")) + public void queuesToDeclareDelete(Message message, @Payload GenericPayloadDto payload) { + log.info( + "Received new message {} in {} (GenericPayloadDto): {}", + message, + AmqpConstants.QUEUE_DELETE, + payload.toString()); + } + + @RabbitListener( + autoStartup = "false", + bindings = + @QueueBinding( + exchange = + @Exchange( + name = AmqpConstants.EXCHANGE_CRUD_TOPIC_EXCHANGE_1, + type = ExchangeTypes.TOPIC), + key = AmqpConstants.ROUTING_KEY_ALL_MESSAGES, + value = @Queue(name = AmqpConstants.QUEUE_UPDATE, durable = "true", autoDelete = "false"))) + public void bindingsUpdate(Message message, @Payload GenericPayloadDto payload) { + log.info( + "Received new message {} in {} (GenericPayloadDto): {}", + message, + AmqpConstants.QUEUE_UPDATE, + payload.toString()); + } + + @RabbitListener( + autoStartup = "false", + bindings = + @QueueBinding( + exchange = + @Exchange( + name = AmqpConstants.EXCHANGE_CRUD_TOPIC_EXCHANGE_2, + type = ExchangeTypes.TOPIC), + key = AmqpConstants.ROUTING_KEY_ALL_MESSAGES, + value = @Queue(name = AmqpConstants.QUEUE_READ, durable = "false", autoDelete = "false"))) + public void bindingsRead(Message message, @Payload ExamplePayloadDto payload) { + log.info( + "Received new message {} in {} (ExamplePayloadDto): {}", + message, + AmqpConstants.QUEUE_UPDATE, + payload.toString()); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/dtos/GenericPayloadDto.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/dtos/GenericPayloadDto.java new file mode 100644 index 000000000..c376517a9 --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/dtos/GenericPayloadDto.java @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.examples.amqp.dtos; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "Generic payload model") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GenericPayloadDto { + + @Schema(description = "Generic Payload field", requiredMode = REQUIRED) + private T genericValue; +} diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java index c2b200ae3..e8cc5cbf0 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java @@ -4,6 +4,7 @@ import io.github.springwolf.bindings.amqp.annotations.AmqpAsyncOperationBinding; import io.github.springwolf.core.asyncapi.annotations.AsyncOperation; import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher; +import io.github.springwolf.examples.amqp.AmqpConstants; import io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto; import lombok.RequiredArgsConstructor; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -17,11 +18,14 @@ public class AnotherProducer { @AsyncPublisher( operation = @AsyncOperation( - channelName = "example-topic-exchange", + channelName = AmqpConstants.EXCHANGE_EXAMPLE_TOPIC_EXCHANGE, description = "Custom, optional description defined in the AsyncPublisher annotation")) @AmqpAsyncOperationBinding() public void sendMessage(AnotherPayloadDto msg) { // send - rabbitTemplate.convertAndSend("example-topic-exchange", "example-topic-routing-key", msg); + rabbitTemplate.convertAndSend( + AmqpConstants.EXCHANGE_EXAMPLE_TOPIC_EXCHANGE, + AmqpConstants.ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY, + msg); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties b/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties index fe58d88d9..b8d3f8122 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties @@ -5,7 +5,7 @@ spring.application.name=Springwolf example project - AMQP ######### # Spring amqp configuration -spring.rabbitmq.host=amqp +spring.rabbitmq.host=${AMQP_HOST:localhost} spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java index 26025d272..75c11c29c 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.test.context.ActiveProfiles; @@ -24,15 +25,41 @@ class ApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; + @Value("${spring.rabbitmq.host}") + public String amqpHost; + + @Value("${spring.rabbitmq.port}") + public String amqpPort; + @Test - void asyncApiResourceArtifactTest() throws IOException { + void asyncApiResourceJsonArtifactTest() throws IOException { String url = "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); - Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); + String actualPatched = actual.replace(amqpHost + ":" + amqpPort, "amqp:5672"); + Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actualPatched); + + String expected; + try (InputStream s = this.getClass().getResourceAsStream("/asyncapi.json")) { + assert s != null; + expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).trim(); + } + + assertEquals(expected, actualPatched); + } + + @Test + void asyncApiResourceYamlArtifactTest() throws IOException { + String url = "/springwolf/docs.yaml"; + String actual = restTemplate.getForObject(url, String.class); + String actualPatched = actual.replace(amqpHost + ":" + amqpPort, "amqp:5672"); + Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.yaml"), actualPatched); - InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); - String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).trim(); + String expected; + try (InputStream s = this.getClass().getResourceAsStream("/asyncapi.yaml")) { + assert s != null; + expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).trim() + "\n"; + } - assertEquals(expected, actual); + assertEquals(expected, actualPatched); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiSystemTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiSystemTest.java index acf71965a..0a9756007 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiSystemTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java index dce74e9f5..d47631301 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java @@ -19,6 +19,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -57,6 +58,7 @@ public class ProducerSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withServices(AMQP_NAME) + .waitingFor(AMQP_NAME, Wait.forLogMessage(".*Server startup complete.*", 1)) .withLogConsumer(AMQP_NAME, l -> log.debug("amqp: {}", l.getUtf8StringWithoutLineEnding())); @Test @@ -79,7 +81,7 @@ void producerCanUseSpringwolfConfigurationToSendMessage() { payload.setSomeEnum(FOO1); // when - springwolfAmqpProducer.send("example-queue", payload); + springwolfAmqpProducer.send(AmqpConstants.QUEUE_EXAMPLE_QUEUE, payload); // then verify(exampleConsumer, timeout(10000)).receiveExamplePayload(payload); diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java index 1554babb9..f38bccbf6 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java @@ -44,7 +44,7 @@ void testContextWithApplicationProperties() { @Test void testAllChannelsAreFound() { - assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(5); + assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(11); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index 72a57381d..8deb3416f 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -25,6 +25,30 @@ } }, "channels": { + "#": { + "address": "#", + "messages": { + "io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + }, + "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + }, + "bindings": { + "amqp": { + "is": "routingKey", + "exchange": { + "name": "CRUD-topic-exchange-1", + "type": "topic", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, "another-queue": { "address": "another-queue", "messages": { @@ -46,6 +70,22 @@ } } }, + "example-bindings-queue": { + "address": "example-bindings-queue", + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "name": "example-bindings-queue", + "durable": false, + "exclusive": false, + "autoDelete": true, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, "example-queue": { "address": "example-queue", "messages": { @@ -119,6 +159,80 @@ "bindingVersion": "0.3.0" } } + }, + "queue-create": { + "address": "queue-create", + "messages": { + "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + }, + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "name": "queue-create", + "durable": true, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, + "queue-delete": { + "address": "queue-delete", + "messages": { + "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + }, + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "name": "queue-delete", + "durable": true, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, + "queue-read": { + "address": "queue-read", + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "name": "queue-read", + "durable": false, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, + "queue-update": { + "address": "queue-update", + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "name": "queue-update", + "durable": true, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } } }, "components": { @@ -216,6 +330,25 @@ "someEnum", "someString" ] + }, + "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto": { + "title": "GenericPayloadDto", + "type": "object", + "properties": { + "genericValue": { + "type": "object", + "description": "Generic Payload field" + } + }, + "description": "Generic payload model", + "examples": [ + { + "genericValue": { } + } + ], + "required": [ + "genericValue" + ] } }, "messages": { @@ -255,10 +388,68 @@ "bindingVersion": "0.3.0" } } + }, + "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto": { + "headers": { + "$ref": "#/components/schemas/SpringRabbitListenerDefaultHeaders" + }, + "payload": { + "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", + "schema": { + "$ref": "#/components/schemas/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + }, + "name": "io.github.springwolf.examples.amqp.dtos.GenericPayloadDto", + "title": "GenericPayloadDto", + "bindings": { + "amqp": { + "bindingVersion": "0.3.0" + } + } } } }, "operations": { + "#_receive_bindingsRead": { + "action": "receive", + "channel": { + "$ref": "#/channels/#" + }, + "bindings": { + "amqp": { + "expiration": 0, + "cc": [ + "#" + ], + "bindingVersion": "0.3.0" + } + }, + "messages": [ + { + "$ref": "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + } + ] + }, + "#_receive_bindingsUpdate": { + "action": "receive", + "channel": { + "$ref": "#/channels/#" + }, + "bindings": { + "amqp": { + "expiration": 0, + "cc": [ + "#" + ], + "bindingVersion": "0.3.0" + } + }, + "messages": [ + { + "$ref": "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + ] + }, "another-queue_receive_receiveAnotherPayload": { "action": "receive", "channel": { @@ -366,6 +557,46 @@ "$ref": "#/channels/multi-payload-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } ] + }, + "queue-create_receive_queuesToDeclareCreate": { + "action": "receive", + "channel": { + "$ref": "#/channels/queue-create" + }, + "bindings": { + "amqp": { + "expiration": 0, + "cc": [ + "queue-create" + ], + "bindingVersion": "0.3.0" + } + }, + "messages": [ + { + "$ref": "#/channels/queue-create/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + ] + }, + "queue-delete_receive_queuesToDeclareDelete": { + "action": "receive", + "channel": { + "$ref": "#/channels/queue-delete" + }, + "bindings": { + "amqp": { + "expiration": 0, + "cc": [ + "queue-delete" + ], + "bindingVersion": "0.3.0" + } + }, + "messages": [ + { + "$ref": "#/channels/queue-delete/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + } + ] } } } \ No newline at end of file diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml new file mode 100644 index 000000000..ca18082d5 --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml @@ -0,0 +1,406 @@ +asyncapi: 3.0.0 +info: + title: Springwolf example project - AMQP + version: 1.0.0 + description: Springwolf example project to demonstrate springwolfs abilities + contact: + name: springwolf + url: https://github.com/springwolf/springwolf-core + email: example@example.com + x-phone: +49 123 456789 + license: + name: Apache License 2.0 + x-desc: some description + x-api-audience: company-internal + x-generator: springwolf +defaultContentType: application/json +servers: + amqp-server: + host: amqp:5672 + protocol: amqp +channels: + '#': + address: "#" + messages: + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + io.github.springwolf.examples.amqp.dtos.GenericPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + bindings: + amqp: + is: routingKey + exchange: + name: CRUD-topic-exchange-1 + type: topic + durable: true + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + another-queue: + address: another-queue + messages: + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + bindings: + amqp: + is: queue + queue: + name: another-queue + durable: false + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + example-bindings-queue: + address: example-bindings-queue + bindings: + amqp: + is: queue + queue: + name: example-bindings-queue + durable: false + exclusive: false + autoDelete: true + vhost: / + bindingVersion: 0.3.0 + example-queue: + address: example-queue + messages: + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + bindings: + amqp: + is: queue + queue: + name: example-queue + durable: false + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + example-topic-exchange: + address: example-topic-exchange + messages: + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + example-topic-routing-key: + address: example-topic-routing-key + messages: + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + bindings: + amqp: + is: routingKey + exchange: + name: example-topic-exchange + type: topic + durable: true + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + multi-payload-queue: + address: multi-payload-queue + messages: + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + bindings: + amqp: + is: queue + queue: + name: multi-payload-queue + durable: true + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + queue-create: + address: queue-create + messages: + io.github.springwolf.examples.amqp.dtos.GenericPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + bindings: + amqp: + is: queue + queue: + name: queue-create + durable: true + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + queue-delete: + address: queue-delete + messages: + io.github.springwolf.examples.amqp.dtos.GenericPayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + bindings: + amqp: + is: queue + queue: + name: queue-delete + durable: true + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + queue-read: + address: queue-read + bindings: + amqp: + is: queue + queue: + name: queue-read + durable: false + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 + queue-update: + address: queue-update + bindings: + amqp: + is: queue + queue: + name: queue-update + durable: true + exclusive: false + autoDelete: false + vhost: / + bindingVersion: 0.3.0 +components: + schemas: + HeadersNotDocumented: + title: HeadersNotDocumented + type: object + properties: {} + description: "There can be headers, but they are not explicitly documented." + examples: + - {} + SpringRabbitListenerDefaultHeaders: + title: SpringRabbitListenerDefaultHeaders + type: object + properties: {} + examples: + - {} + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + title: AnotherPayloadDto + type: object + properties: + example: + $ref: "#/components/schemas/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + foo: + type: string + description: Foo field + maxLength: 100 + examples: + - bar + description: Another payload model + examples: + - example: + someEnum: FOO2 + someLong: 5 + someString: some string value + foo: bar + required: + - example + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + title: ExamplePayloadDto + type: object + properties: + someEnum: + type: string + description: Some enum field + enum: + - FOO1 + - FOO2 + - FOO3 + examples: + - FOO2 + someLong: + type: integer + description: Some long field + format: int64 + minimum: 0 + examples: + - 5 + someString: + type: string + description: Some string field + examples: + - some string value + description: Example payload model + examples: + - someEnum: FOO2 + someLong: 5 + someString: some string value + required: + - someEnum + - someString + io.github.springwolf.examples.amqp.dtos.GenericPayloadDto: + title: GenericPayloadDto + type: object + properties: + genericValue: + type: object + description: Generic Payload field + description: Generic payload model + examples: + - genericValue: {} + required: + - genericValue + messages: + io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: + headers: + $ref: "#/components/schemas/HeadersNotDocumented" + payload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + $ref: "#/components/schemas/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + name: io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto + title: AnotherPayloadDto + description: Another payload model + bindings: + amqp: + bindingVersion: 0.3.0 + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + headers: + $ref: "#/components/schemas/SpringRabbitListenerDefaultHeaders" + payload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + $ref: "#/components/schemas/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + name: io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto + title: ExamplePayloadDto + bindings: + amqp: + bindingVersion: 0.3.0 + io.github.springwolf.examples.amqp.dtos.GenericPayloadDto: + headers: + $ref: "#/components/schemas/SpringRabbitListenerDefaultHeaders" + payload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + $ref: "#/components/schemas/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + name: io.github.springwolf.examples.amqp.dtos.GenericPayloadDto + title: GenericPayloadDto + bindings: + amqp: + bindingVersion: 0.3.0 +operations: + '#_receive_bindingsRead': + action: receive + channel: + $ref: "#/channels/#" + bindings: + amqp: + expiration: 0 + cc: + - "#" + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + '#_receive_bindingsUpdate': + action: receive + channel: + $ref: "#/channels/#" + bindings: + amqp: + expiration: 0 + cc: + - "#" + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + another-queue_receive_receiveAnotherPayload: + action: receive + channel: + $ref: "#/channels/another-queue" + bindings: + amqp: + expiration: 0 + cc: + - another-queue + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/another-queue/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + example-queue_receive_receiveExamplePayload: + action: receive + channel: + $ref: "#/channels/example-queue" + bindings: + amqp: + expiration: 0 + cc: + - example-queue + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/example-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + example-topic-exchange_send_sendMessage: + action: send + channel: + $ref: "#/channels/example-topic-exchange" + title: example-topic-exchange_send + description: "Custom, optional description defined in the AsyncPublisher annotation" + bindings: + amqp: + expiration: 0 + cc: [] + priority: 0 + deliveryMode: 1 + mandatory: false + timestamp: false + ack: false + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/example-topic-exchange/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + example-topic-routing-key_receive_bindingsExample: + action: receive + channel: + $ref: "#/channels/example-topic-routing-key" + bindings: + amqp: + expiration: 0 + cc: + - example-topic-routing-key + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/example-topic-routing-key/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + multi-payload-queue_receive_bindingsBeanExample: + action: receive + channel: + $ref: "#/channels/multi-payload-queue" + bindings: + amqp: + expiration: 0 + cc: + - multi-payload-queue + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/multi-payload-queue/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + - $ref: "#/channels/multi-payload-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + queue-create_receive_queuesToDeclareCreate: + action: receive + channel: + $ref: "#/channels/queue-create" + bindings: + amqp: + expiration: 0 + cc: + - queue-create + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/queue-create/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" + queue-delete_receive_queuesToDeclareDelete: + action: receive + channel: + $ref: "#/channels/queue-delete" + bindings: + amqp: + expiration: 0 + cc: + - queue-delete + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/queue-delete/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDto" diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/springwolf/examples/cloudstream/ApiSystemTest.java b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/springwolf/examples/cloudstream/ApiSystemTest.java index 6845d6753..f8e4b823f 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/springwolf/examples/cloudstream/ApiSystemTest.java +++ b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/springwolf/examples/cloudstream/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/springwolf/examples/jms/ApiSystemTest.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/springwolf/examples/jms/ApiSystemTest.java index 8c458f201..f6a6a3d4a 100644 --- a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/springwolf/examples/jms/ApiSystemTest.java +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/springwolf/examples/jms/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiSystemTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiSystemTest.java index 48319c403..053cf52aa 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiSystemTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/springwolf/examples/kafka/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/springwolf/examples/sns/ApiSystemTest.java b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/springwolf/examples/sns/ApiSystemTest.java index 415ce0ea0..ba9c856e6 100644 --- a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/springwolf/examples/sns/ApiSystemTest.java +++ b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/springwolf/examples/sns/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/springwolf/examples/sqs/ApiSystemTest.java b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/springwolf/examples/sqs/ApiSystemTest.java index e6f6130de..bdae15593 100644 --- a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/springwolf/examples/sqs/ApiSystemTest.java +++ b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/springwolf/examples/sqs/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -47,6 +48,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-examples/springwolf-stomp-example/src/test/java/io/github/springwolf/examples/stomp/stomp/ApiSystemTest.java b/springwolf-examples/springwolf-stomp-example/src/test/java/io/github/springwolf/examples/stomp/stomp/ApiSystemTest.java index 4b01a5fce..d8a8d5723 100644 --- a/springwolf-examples/springwolf-stomp-example/src/test/java/io/github/springwolf/examples/stomp/stomp/ApiSystemTest.java +++ b/springwolf-examples/springwolf-stomp-example/src/test/java/io/github/springwolf/examples/stomp/stomp/ApiSystemTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -45,6 +46,7 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) + .waitingFor(APP_NAME, Wait.forLogMessage(".*AsyncAPI document was built.*", 1)) .withEnv(ENV) .withLogConsumer(APP_NAME, l -> log.debug("APP: {}", l.getUtf8StringWithoutLineEnding())); diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java index 09a6200a3..e5d925482 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java @@ -11,6 +11,8 @@ import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelType; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPMessageBinding; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPOperationBinding; +import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; +import io.github.springwolf.asyncapi.v3.model.channel.ChannelObject; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Exchange; @@ -33,6 +35,7 @@ @Slf4j public class RabbitListenerUtil { + public static final String BINDING_NAME = "amqp"; private static final Boolean DEFAULT_AUTO_DELETE = false; private static final Boolean DEFAULT_DURABLE = true; private static final Boolean DEFAULT_EXCLUSIVE = false; @@ -98,7 +101,7 @@ public static Map buildChannelBinding( channelBinding.exchange(buildExchangeProperties(annotation, exchangeName, context)); } - return Map.of("amqp", channelBinding.build()); + return Map.of(BINDING_NAME, channelBinding.build()); } private static AMQPChannelExchangeProperties buildExchangeProperties( @@ -163,6 +166,24 @@ private static AMQPChannelQueueProperties buildQueueProperties( .build(); } + public static ChannelObject buildChannelObject(org.springframework.amqp.core.Queue queue) { + return ChannelObject.builder() + .channelId(ReferenceUtil.toValidId(queue.getName())) + .address(queue.getName()) + .bindings(Map.of( + BINDING_NAME, + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name(queue.getName()) + .autoDelete(queue.isAutoDelete()) + .durable(queue.isDurable()) + .exclusive(queue.isExclusive()) + .build()) + .build())) + .build(); + } + private static Boolean parse(String value, Boolean defaultIfEmpty) { if ("".equals(value)) { return defaultIfEmpty; @@ -194,7 +215,7 @@ private static String getExchangeName( public static Map buildOperationBinding( RabbitListener annotation, StringValueResolver resolver, RabbitListenerUtilContext context) { return Map.of( - "amqp", + BINDING_NAME, AMQPOperationBinding.builder() .cc(getRoutingKeys(annotation, resolver, context)) .build()); @@ -234,7 +255,7 @@ private static List getRoutingKeys( public static Map buildMessageBinding() { // currently the feature to define amqp message binding is not implemented. - return Map.of("amqp", new AMQPMessageBinding()); + return Map.of(BINDING_NAME, new AMQPMessageBinding()); } public record RabbitListenerUtilContext( diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java new file mode 100644 index 000000000..79ef8726a --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.plugins.amqp.asyncapi.scanners.channels; + +import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelBinding; +import io.github.springwolf.asyncapi.v3.model.channel.ChannelObject; +import io.github.springwolf.core.asyncapi.scanners.ChannelsScanner; +import io.github.springwolf.plugins.amqp.asyncapi.scanners.bindings.RabbitListenerUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.core.Queue; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class RabbitQueueBeanScanner implements ChannelsScanner { + private final List queues; + + @Override + public Map scan() { + return queues.stream() + .map(RabbitListenerUtil::buildChannelObject) + .collect(Collectors.toMap( + o -> ((AMQPChannelBinding) o.getBindings().get(RabbitListenerUtil.BINDING_NAME)) + .getQueue() + .getName(), + c -> c, + (a, b) -> a)); + } +} diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java index fa02891d0..67f84d920 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java @@ -14,6 +14,7 @@ import io.github.springwolf.core.asyncapi.scanners.operations.annotations.SpringAnnotationClassLevelOperationsScanner; import io.github.springwolf.core.asyncapi.scanners.operations.annotations.SpringAnnotationMethodLevelOperationsScanner; import io.github.springwolf.plugins.amqp.asyncapi.scanners.bindings.AmqpBindingFactory; +import io.github.springwolf.plugins.amqp.asyncapi.scanners.channels.RabbitQueueBeanScanner; import io.github.springwolf.plugins.amqp.asyncapi.scanners.common.headers.AsyncHeadersForAmqpBuilder; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Exchange; @@ -158,4 +159,14 @@ public SpringAnnotationOperationsScanner simpleRabbitMethodLevelListenerAnnotati return new SpringAnnotationOperationsScanner(springwolfClassScanner, strategy); } + + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_RABBIT_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public RabbitQueueBeanScanner rabbitQueueBeanScanner(List queues) { + return new RabbitQueueBeanScanner(queues); + } } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java new file mode 100644 index 000000000..d12d8244b --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.plugins.amqp.asyncapi.scanners.channels; + +import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelBinding; +import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelQueueProperties; +import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelType; +import io.github.springwolf.asyncapi.v3.model.channel.ChannelObject; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Queue; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class RabbitQueueBeanScannerTest { + + @Test + void scan() { + // given + var queue = new Queue("name"); + var scanner = new RabbitQueueBeanScanner(List.of(queue)); + + // when + var result = scanner.scan(); + + // then + assertThat(result) + .isEqualTo(Map.of( + "name", + ChannelObject.builder() + .channelId("name") + .address("name") + .bindings(Map.of( + "amqp", + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name("name") + .autoDelete(false) + .durable(true) + .exclusive(false) + .build()) + .build())) + .build())); + } +} diff --git a/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.spec.ts b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.spec.ts index 813afd2b9..dde13b8f8 100644 --- a/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.spec.ts +++ b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.spec.ts @@ -12,7 +12,9 @@ import { AsyncApiService } from "../../../service/asyncapi/asyncapi.service"; import { PublisherService } from "../../../service/publisher.service"; describe("ChannelMainComponent", () => { - const mockData = mockedExampleSchemaMapped.channelOperations[0]; + const mockData = mockedExampleSchemaMapped.channelOperations + .slice(-1) + .pop()!!; beforeEach(async () => { mockedAsyncApiService.getAsyncApi.mockClear(); diff --git a/springwolf-ui/src/app/components/new/channels/channel-main/channel-operation.component.spec.ts b/springwolf-ui/src/app/components/new/channels/channel-main/channel-operation.component.spec.ts index 4a59e7f4e..bd8fa88c4 100644 --- a/springwolf-ui/src/app/components/new/channels/channel-main/channel-operation.component.spec.ts +++ b/springwolf-ui/src/app/components/new/channels/channel-main/channel-operation.component.spec.ts @@ -16,7 +16,9 @@ import { } from "../../../mock-components.spec"; describe("ChannelOperationComponent", () => { - const mockData = mockedExampleSchemaMapped.channelOperations[0]; + const mockData = mockedExampleSchemaMapped.channelOperations + .slice(-1) + .pop()!!; beforeEach(async () => { mockedAsyncApiService.getAsyncApi.mockClear(); diff --git a/springwolf-ui/src/app/components/new/channels/channels.component.html b/springwolf-ui/src/app/components/new/channels/channels.component.html index 7ee69ca09..b213b6100 100644 --- a/springwolf-ui/src/app/components/new/channels/channels.component.html +++ b/springwolf-ui/src/app/components/new/channels/channels.component.html @@ -3,6 +3,22 @@

Channels

@for (channel of channels; track channel) {

{{ channel.name }}

+ +
+
+
Channel Binding
+
+
+
+ +
+
+
+ @for (channelOperation of channel.operations; track channelOperation) { { this.channels = asyncapi.channels; }); + + this.uiService.isShowBindings$.subscribe( + (value) => (this.isShowBindings = value) + ); } } diff --git a/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts b/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts index 612aa0496..509fe78ca 100644 --- a/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts +++ b/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts @@ -258,7 +258,7 @@ export class AsyncApiMapperService { "message of channel " + channelName, () => { const messageId = this.resolveRefId(operationMessage.$ref); - const channelMessage = channel.messages[messageId]; + const channelMessage = channel.messages!![messageId]; const channelMessageId = this.resolveRefId(channelMessage.$ref); const message = messages[channelMessageId]; diff --git a/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts b/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts index caae40d9f..0a538c6dc 100644 --- a/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts +++ b/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts @@ -8,7 +8,7 @@ export interface ServerChannels { export interface ServerChannel { address: string; description?: string; - messages: { + messages?: { [key: string]: { $ref: string; }; diff --git a/springwolf-ui/src/app/service/mock/mock-asyncapi.service.ts b/springwolf-ui/src/app/service/mock/mock-asyncapi.service.ts index 6c140ad01..88be32023 100644 --- a/springwolf-ui/src/app/service/mock/mock-asyncapi.service.ts +++ b/springwolf-ui/src/app/service/mock/mock-asyncapi.service.ts @@ -8,7 +8,7 @@ const asyncApiMapperService = new AsyncApiMapperService({ showWarning: jest.fn(), }); export const mockedExampleSchemaMapped = asyncApiMapperService.toAsyncApi( - exampleSchemas[0].value + exampleSchemas.find((el) => el.plugin === "kafka")!!.value )!!; export const mockedAsyncApiService: { getAsyncApi: jest.Mock } = { getAsyncApi: jest.fn().mockReturnValue(of(mockedExampleSchemaMapped)),