From 67e184ecc660f9a40a4ef4e9c844dd8a358852cf Mon Sep 17 00:00:00 2001 From: Gretchen-z Date: Fri, 12 Sep 2025 11:36:13 +0300 Subject: [PATCH 1/4] Add WebhookControllerWithInterceptorTest --- .../WebhookControllerWithInterceptorTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java new file mode 100644 index 0000000..c630eb6 --- /dev/null +++ b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java @@ -0,0 +1,111 @@ +package dev.vality.alerting.tg.bot; + +import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; +import dev.vality.alerting.tg.bot.controller.WebhookController; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(WebhookController.class) +@Import(WebhookControllerWithInterceptorTest.TestMvcConfig.class) +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(properties = { + "spring.mvc.pathmatch.matching-strategy=ant_path_matcher", + "bot.token=test", + "bot.name=vality_alerting_bot", + "bot.chatId=1", + "bot.topics.commands=1", + "bot.topics.errors5xx=2", + "bot.topics.altpay-conversion=3", + "bot.topics.failed-machines=4", + "bot.topics.pending-payments=5" +}) +class WebhookControllerWithInterceptorTest { + + @Autowired MockMvc mvc; + + @MockitoBean + TelegramSender telegramSender; + + @MockitoBean + AlertmanagerWebhookProperties alertmanagerWebhookProperties; + + @Autowired + RequestMappingHandlerMapping mappings; + + @Test + void mappingExists_sanityCheck() { + var exists = mappings.getHandlerMethods().keySet().stream() + .anyMatch(i -> i.getPatternsCondition().getPatterns().stream() + .anyMatch(p -> p.equals("/alertmanager/webhook"))); + assertThat(exists) + .as("Контроллер смапил /alertmanager/webhook") + .isTrue(); + } + + @Test + void whenWebhookPosted_thenTestInterceptorInvokesTelegramSender() throws Exception { + String json = """ + { "status":"firing", "alerts":[ { "status":"firing" } ] } + """; + + mvc.perform(post("/alertmanager/webhook") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()); + + verify(telegramSender, times(1)) + .send(argThat(s -> s.contains("webhook"))); + } + + @TestConfiguration + static class TestMvcConfig implements WebMvcConfigurer { + + @Autowired TelegramSender telegramSender; + + @Bean + HandlerInterceptor webhookNotifyInterceptor() { + return new HandlerInterceptor() { + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { + if (request.getRequestURI().equals("/alertmanager/webhook") && response.getStatus() == 200) { + telegramSender.send("alertmanager webhook handled"); + } + } + }; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(webhookNotifyInterceptor()) + .addPathPatterns("/alertmanager/webhook"); + } + } + + interface TelegramSender { + void send(String text); + } +} From 4fc0eb9c41fe70f33f5a2e9682ec5abc76e7a11b Mon Sep 17 00:00:00 2001 From: Gretchen-z Date: Fri, 12 Sep 2025 11:47:35 +0300 Subject: [PATCH 2/4] Fix checkstyle WebhookControllerWithInterceptorTest --- .../tg/bot/WebhookControllerWithInterceptorTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java index c630eb6..b27d394 100644 --- a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java +++ b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java @@ -69,8 +69,8 @@ void mappingExists_sanityCheck() { @Test void whenWebhookPosted_thenTestInterceptorInvokesTelegramSender() throws Exception { String json = """ - { "status":"firing", "alerts":[ { "status":"firing" } ] } - """; + { "status":"firing", "alerts":[ { "status":"firing" } ] } + """; mvc.perform(post("/alertmanager/webhook") .contentType(MediaType.APPLICATION_JSON) @@ -90,7 +90,10 @@ static class TestMvcConfig implements WebMvcConfigurer { HandlerInterceptor webhookNotifyInterceptor() { return new HandlerInterceptor() { @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, + @Nullable Exception ex) { if (request.getRequestURI().equals("/alertmanager/webhook") && response.getStatus() == 200) { telegramSender.send("alertmanager webhook handled"); } From d6c6a501ade489c34d41f84bc7bc1a8643a14cd5 Mon Sep 17 00:00:00 2001 From: Gretchen-z Date: Fri, 12 Sep 2025 12:00:44 +0300 Subject: [PATCH 3/4] Fix test names WebhookControllerWithInterceptorTest --- .../alerting/tg/bot/WebhookControllerWithInterceptorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java index b27d394..f262079 100644 --- a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java +++ b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java @@ -57,7 +57,7 @@ class WebhookControllerWithInterceptorTest { RequestMappingHandlerMapping mappings; @Test - void mappingExists_sanityCheck() { + void mappingExistSanityCheck() { var exists = mappings.getHandlerMethods().keySet().stream() .anyMatch(i -> i.getPatternsCondition().getPatterns().stream() .anyMatch(p -> p.equals("/alertmanager/webhook"))); @@ -67,7 +67,7 @@ void mappingExists_sanityCheck() { } @Test - void whenWebhookPosted_thenTestInterceptorInvokesTelegramSender() throws Exception { + void whenWebhookPostedThenTestInterceptorInvokesTelegramSender() throws Exception { String json = """ { "status":"firing", "alerts":[ { "status":"firing" } ] } """; From 7c0e301570d5c519aa311fb142099b426e3f6d92 Mon Sep 17 00:00:00 2001 From: Gretchen-z Date: Fri, 12 Sep 2025 15:04:36 +0300 Subject: [PATCH 4/4] Fix test for WebhookController --- pom.xml | 6 + .../tg/bot/controller/WebhookController.java | 4 +- .../vality/alerting/tg/bot/model/Webhook.java | 6 + .../alerting/tg/bot/service/AlertBot.java | 6 + .../tg/bot/WebhookControllerTest.java | 108 +++++++++++++++++ .../WebhookControllerWithInterceptorTest.java | 114 ------------------ 6 files changed, 129 insertions(+), 115 deletions(-) create mode 100644 src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java delete mode 100644 src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java diff --git a/pom.xml b/pom.xml index c8a4e42..815011f 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ spring-boot-starter-test test + + org.springframework.cloud + spring-cloud-contract-wiremock + 4.1.4 + test + diff --git a/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java b/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java index 5bc942c..3f22fab 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java +++ b/src/main/java/dev/vality/alerting/tg/bot/controller/WebhookController.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; import dev.vality.alerting.tg.bot.model.Webhook; +import dev.vality.alerting.tg.bot.service.AlertBot; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +23,7 @@ public class WebhookController { private final AlertmanagerWebhookProperties alertmanagerWebhookProperties; private final ObjectMapper objectMapper; - public static final String FIRING = "firing"; + private final AlertBot alertBot; @PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity processWebhook(HttpServletRequest servletRequest) { @@ -30,6 +31,7 @@ public ResponseEntity processWebhook(HttpServletRequest servletRequest) var webhookBody = servletRequest.getReader().lines().collect(Collectors.joining(" ")); log.info("Received webhook from alertmanager: {}", webhookBody); var webhook = objectMapper.readValue(webhookBody, Webhook.class); + alertBot.sendAlertMessage(webhook); } catch (Exception e) { log.error("Unexpected error during webhook parsing:", e); return ResponseEntity.internalServerError().build(); diff --git a/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java b/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java index 19253ed..b426c5f 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java +++ b/src/main/java/dev/vality/alerting/tg/bot/model/Webhook.java @@ -1,6 +1,8 @@ package dev.vality.alerting.tg.bot.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; @@ -9,6 +11,8 @@ * Alertmanager webhook body */ @Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Webhook { private String status; @@ -16,6 +20,8 @@ public class Webhook { private List alerts; @Data + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) public static class Alert { private String status; diff --git a/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java b/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java index 86b7e93..912ab21 100644 --- a/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java +++ b/src/main/java/dev/vality/alerting/tg/bot/service/AlertBot.java @@ -1,6 +1,7 @@ package dev.vality.alerting.tg.bot.service; import dev.vality.alerting.tg.bot.config.properties.AlertBotProperties; +import dev.vality.alerting.tg.bot.model.Webhook; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.EnableScheduling; @@ -68,6 +69,11 @@ public void onUpdateReceived(Update update) { } } + public void sendAlertMessage(Webhook webhook) { + sendResponse(properties.getChatId(), properties.getTopics().getCommands(), webhook.getAlerts().toString(), + null); + } + public void sendScheduledMetrics() { send5xxErrorsMetrics(properties.getChatId()); sendFailedMachinesMetrics(properties.getChatId()); diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java new file mode 100644 index 0000000..35582e4 --- /dev/null +++ b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerTest.java @@ -0,0 +1,108 @@ +package dev.vality.alerting.tg.bot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.alerting.tg.bot.config.AlertBotConfig; +import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; +import dev.vality.alerting.tg.bot.controller.WebhookController; +import dev.vality.alerting.tg.bot.model.Webhook; +import dev.vality.alerting.tg.bot.service.AlertBot; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@TestPropertySource(properties = { + "spring.mvc.pathmatch.matching-strategy=ant_path_matcher", + "bot.token=test", + "bot.name=vality_alerting_bot", + "bot.chatId=1", + "bot.topics.commands=1", + "bot.topics.errors5xx=2", + "bot.topics.altpay-conversion=3", + "bot.topics.failed-machines=4", + "bot.topics.pending-payments=5" +}) +public class WebhookControllerTest { + + @MockitoBean + AlertmanagerWebhookProperties webhookProperties; + + @MockitoBean + AlertBot alertBot; + + @MockitoBean + AlertBotConfig alertBotConfig; + + String webhookJson = """ + { + "status": "firing", + "receiver": "telegram", + "alerts": [ + { + "status": "firing", + "labels": { + "alertname": "Errors5xxHigh", + "severity": "critical", + "job": "payments", + "namespace": "prod", + "service": "payments-api", + "instance": "payments-api-1", + "pod": "payments-api-1-abc123" + }, + "annotations": { + "summary": "HTTP 5xx rate is too high", + "description": "Payments API is returning >5% 5xx responses for 5m", + "runbook_url": "https://runbook.company/alerts/errors5xx" + } + }, + { + "status": "resolved", + "labels": { + "alertname": "AltpayConversionLow", + "severity": "warning", + "job": "altpay", + "namespace": "prod", + "service": "altpay-conversion", + "pod": "altpay-0-xzy987" + }, + "annotations": { + "summary": "Altpay conversion dropped", + "description": "Altpay conversion < 2% in last 10m" + } + } + ] + } + """; + + @Test + public void sendTgMessageTest() { + ObjectMapper objectMapper = new ObjectMapper(); + WebhookController webhookController = new WebhookController(webhookProperties, objectMapper, alertBot); + + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setMethod("POST"); + req.setRequestURI("/alertmanager/webhook"); + req.setContentType(MediaType.APPLICATION_JSON_VALUE); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setContent(webhookJson.getBytes(StandardCharsets.UTF_8)); + + val response = webhookController.processWebhook(req); + assertThat(response.getStatusCode().value()).isEqualTo(200); + + ArgumentCaptor webhookCaptor = ArgumentCaptor.forClass(Webhook.class); + verify(alertBot).sendAlertMessage(webhookCaptor.capture()); + Webhook passed = webhookCaptor.getValue(); + assertThat(passed).isNotNull(); + assertThat(passed.getAlerts()).isNotNull(); + } +} diff --git a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java b/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java deleted file mode 100644 index f262079..0000000 --- a/src/test/java/dev/vality/alerting/tg/bot/WebhookControllerWithInterceptorTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.vality.alerting.tg.bot; - -import dev.vality.alerting.tg.bot.config.properties.AlertmanagerWebhookProperties; -import dev.vality.alerting.tg.bot.controller.WebhookController; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(WebhookController.class) -@Import(WebhookControllerWithInterceptorTest.TestMvcConfig.class) -@AutoConfigureMockMvc(addFilters = false) -@TestPropertySource(properties = { - "spring.mvc.pathmatch.matching-strategy=ant_path_matcher", - "bot.token=test", - "bot.name=vality_alerting_bot", - "bot.chatId=1", - "bot.topics.commands=1", - "bot.topics.errors5xx=2", - "bot.topics.altpay-conversion=3", - "bot.topics.failed-machines=4", - "bot.topics.pending-payments=5" -}) -class WebhookControllerWithInterceptorTest { - - @Autowired MockMvc mvc; - - @MockitoBean - TelegramSender telegramSender; - - @MockitoBean - AlertmanagerWebhookProperties alertmanagerWebhookProperties; - - @Autowired - RequestMappingHandlerMapping mappings; - - @Test - void mappingExistSanityCheck() { - var exists = mappings.getHandlerMethods().keySet().stream() - .anyMatch(i -> i.getPatternsCondition().getPatterns().stream() - .anyMatch(p -> p.equals("/alertmanager/webhook"))); - assertThat(exists) - .as("Контроллер смапил /alertmanager/webhook") - .isTrue(); - } - - @Test - void whenWebhookPostedThenTestInterceptorInvokesTelegramSender() throws Exception { - String json = """ - { "status":"firing", "alerts":[ { "status":"firing" } ] } - """; - - mvc.perform(post("/alertmanager/webhook") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isOk()); - - verify(telegramSender, times(1)) - .send(argThat(s -> s.contains("webhook"))); - } - - @TestConfiguration - static class TestMvcConfig implements WebMvcConfigurer { - - @Autowired TelegramSender telegramSender; - - @Bean - HandlerInterceptor webhookNotifyInterceptor() { - return new HandlerInterceptor() { - @Override - public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, - @Nullable Exception ex) { - if (request.getRequestURI().equals("/alertmanager/webhook") && response.getStatus() == 200) { - telegramSender.send("alertmanager webhook handled"); - } - } - }; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(webhookNotifyInterceptor()) - .addPathPatterns("/alertmanager/webhook"); - } - } - - interface TelegramSender { - void send(String text); - } -}