Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class DeadLetterConsumer {
* Consumes messages rejected by the main listener after all retry attempts.
* Logs a structured ERROR for Loki/Grafana alerting and acks the message.
*/
@RabbitListener(queues = RabbitMQConfig.DLQ)
@RabbitListener(id = "dlqListener", queues = RabbitMQConfig.DLQ)
public void consumeDeadLetter(OrderPlacedEvent event) {
log.error("DEAD_LETTER_RECEIVED: orderReference={}, email={}",
event.orderReference(), event.email());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import com.ecommerce.notification_service.event.OrderPlacedEvent;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.MailSendException;
Expand All @@ -33,8 +35,8 @@
* - RabbitMQ → Testcontainers (real broker; DLX/DLQ declared by Spring context)
* - JavaMailSender → @MockitoBean (throws MailSendException on every send)
*
* Retry intervals are overridden via @DynamicPropertySource to keep the test fast
* (2 attempts × ~100 ms intervals instead of the production 3 × 10 s).
* The DeadLetterConsumer listener is stopped before the test so messages remain
* in the DLQ and can be polled directly via rabbitTemplate.receive().
*/
@SpringBootTest
@Testcontainers
Expand All @@ -56,8 +58,14 @@ static void configureProperties(DynamicPropertyRegistry registry) {

registry.add("spring.mail.host", () -> "127.0.0.1");
registry.add("spring.mail.port", () -> "25");
// @MockitoBean replaces JavaMailSenderImpl with a mock; the mail health
// contributor requires a concrete implementation, so disable it here.
registry.add("management.health.mail.enabled", () -> "false");

// Fast retries so the test doesn't wait ~19 s (production: 3 attempts × 10 s)
// Retry is disabled by default in the test classpath application.yaml (which replaces
// the production yaml). Explicitly enable it and use fast intervals so the test
// exhausts retries quickly instead of waiting ~19 s (production: 3 attempts × 10 s).
registry.add("spring.rabbitmq.listener.simple.retry.enabled", () -> "true");
registry.add("spring.rabbitmq.listener.simple.retry.max-attempts", () -> "2");
registry.add("spring.rabbitmq.listener.simple.retry.initial-interval", () -> "100ms");
registry.add("spring.rabbitmq.listener.simple.retry.max-interval", () -> "200ms");
Expand All @@ -67,6 +75,18 @@ static void configureProperties(DynamicPropertyRegistry registry) {
@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private RabbitListenerEndpointRegistry listenerRegistry;

@BeforeEach
void stopDlqConsumer() {
// Stop the DeadLetterConsumer listener so messages stay in the DLQ
// and can be polled directly. Spring AMQP captures the original bean
// reference before any spy/mock wrapper, making spy-based verification
// unreliable — polling a paused queue is the reliable alternative.
listenerRegistry.getListenerContainer("dlqListener").stop();
}

@Test
void publishOrderEvent_emailAlwaysFails_messageDeadLetters() {
// Arrange – simulate SMTP rejection after message construction succeeds
Expand All @@ -84,7 +104,7 @@ void publishOrderEvent_emailAlwaysFails_messageDeadLetters() {
event);

// Assert – wait up to 15 s for the dead-lettered message to appear in the DLQ.
// rabbitTemplate.receive() does a non-blocking peek with a 1-second timeout.
// The DLQ consumer is stopped so the message stays until we receive it.
await()
.atMost(15, TimeUnit.SECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
Expand Down
Loading