Skip to content
Open
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/qa-guru/allure-notifications/blob/master/README.en.md)

# Allure notifications
**Allure notifications** - это библиотека, позволяющая выполнять автоматическое оповещение о результатах прохождения автотестов, которое направляется в нужный вам мессенджер (Telegram, Slack, ~~Skype~~, Email, Mattermost, Discord, Loop, Rocket.Chat, Zoho Cliq).
**Allure notifications** - это библиотека, позволяющая выполнять автоматическое оповещение о результатах прохождения автотестов, которое направляется в нужный вам мессенджер (Telegram, Slack, ~~Skype~~, Email, Mattermost, Discord, Loop, Rocket.Chat, Zoho Cliq, Gitlab).

Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳

Expand Down Expand Up @@ -141,6 +141,15 @@ Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳
"port": 0,
"username": "",
"password": ""
},
"gitlab": {
"enabled": true,
"url": "",
"apiKey": "",
"apiToken": "",
"projectId": "",
"mergeRequestIid": "",
"templatePath": "/templates/gitlab.ftl"
}
}
```
Expand Down Expand Up @@ -351,3 +360,18 @@ java "-DconfigFile=notifications/config.json" -jar ../allure-notifications-4.2.1
</ul>
Для получения дополнительной информации об API Zoho Cliq посетите <a href="https://www.zoho.com/cliq/help/restapi/v2/" target="_blank">официальную документацию</a>.
</details>
+ <details>
<summary>gitlab</summary>
Поддерживается публикация отчета в комментариях к merge request. Для публикации необходимо указать следующие параметры конфигурации:
<ul>
<li><code>enabled</code> - признак выполнения публикации. Если ключ отсутствует или равен true, то публикация выполняется</li>
<li><code>url</code> - адрес сервера gitlab</li>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add an example

<li><code>apiKey</code> - имя пользователя для аутентификации</li>
<li><code>apiToken</code> - токен для аутентификации</li>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how to get token?

<li><code>projectId</code> - идентификатор проекта в gitlab</li>
<li><code>mergeRequestIid</code> - номер (iid) мерж реквеста в gitlab</li>
</ul>
Отчет публикуется в новом комментарии к мерж реквесту, на который указывает параметр <code>mergeRequestIid</code>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add the same information to README.en.md

</details>


Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package guru.qa.allure.notifications.clients;

import guru.qa.allure.notifications.clients.gitlab.GitlabClient;
import guru.qa.allure.notifications.clients.rocket.RocketChatClient;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -41,6 +42,9 @@ public static List<Notifier> from(Config config) {
if (config.getCliq() != null) {
notifiers.add(new CliqClient(config.getCliq(), config.getProxy()));
}
if (config.getGitlab() != null && Boolean.TRUE.equals(config.getGitlab().getEnabled())) {
notifiers.add(new GitlabClient(config.getGitlab()));
}
return notifiers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package guru.qa.allure.notifications.clients.gitlab;

import guru.qa.allure.notifications.clients.Notifier;
import guru.qa.allure.notifications.config.gitlab.Gitlab;
import guru.qa.allure.notifications.exceptions.MessagingException;
import guru.qa.allure.notifications.template.MessageTemplate;
import guru.qa.allure.notifications.template.data.MessageData;
import kong.unirest.ContentType;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class GitlabClient implements Notifier {
private final Gitlab gitlab;

public GitlabClient(Gitlab gitlab) {
this.gitlab = gitlab;
}

@Override
public void sendText(MessageData messageData) throws MessagingException {
send(messageData);
}

@Override
public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException {
JsonNode jsonBody = Unirest.post(String.format("%s/api/v4/projects/{projectId}/uploads", gitlab.getUrl()))
.routeParam("projectId", gitlab.getProjectId())
.header(gitlab.getApiKey(), gitlab.getApiToken())
.field("file", new ByteArrayInputStream(chartImage), ContentType.APPLICATION_OCTET_STREAM, "file.png")
.asJson()
.getBody();

jsonBody.getObject().toMap().computeIfPresent("markdown", (k, v) -> messageData.getValues().put("chartSource", v));

send(messageData);
}

private void send(MessageData messageData) throws MessagingException {
Map<String, Object> body = new HashMap<>();
body.put("body", MessageTemplate.createMessageFromTemplate(messageData, gitlab.getTemplatePath()));
Unirest.post(String.format("%s/api/v4/projects/{projectId}/merge_requests/{mergeRequestIid}/notes", gitlab.getUrl()))
.routeParam("projectId", gitlab.getProjectId())
.routeParam("mergeRequestIid", gitlab.getMergeRequestIid())
.header(gitlab.getApiKey(), gitlab.getApiToken())
.header("Content-Type", ContentType.APPLICATION_JSON.getMimeType())
.body(body)
.asString()
.getBody();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import guru.qa.allure.notifications.config.base.Base;
import guru.qa.allure.notifications.config.cliq.Cliq;
import guru.qa.allure.notifications.config.discord.Discord;
import guru.qa.allure.notifications.config.gitlab.Gitlab;
import guru.qa.allure.notifications.config.loop.Loop;
import guru.qa.allure.notifications.config.mail.Mail;
import guru.qa.allure.notifications.config.mattermost.Mattermost;
Expand All @@ -29,4 +30,5 @@ public class Config {
private RocketChat rocketChat;
private Cliq cliq;
private Proxy proxy;
private Gitlab gitlab;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package guru.qa.allure.notifications.config.gitlab;

import lombok.Data;

@Data
public class Gitlab {
private Boolean enabled = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this flag needed? no configuration for Gitlab results in no Gitlab invocations

private String url;
private String apiKey;
private String apiToken;
private String projectId;
private String mergeRequestIid;
private String templatePath = "/templates/gitlab.ftl";
}
45 changes: 45 additions & 0 deletions allure-notifications-api/src/main/resources/templates/gitlab.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<#import "utils.ftl" as utils>
<#compress>
*${phrases.results}:* <br>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between html.ftl and this template?

*${phrases.environment}:* ${environment} <br>
*${phrases.comment}:* ${comment} <br>
*${phrases.scenario.duration}:* ${time} <br>
*${phrases.scenario.totalScenarios}:* ${statistic.total} <br>
<#if statistic.passed != 0 >
*${phrases.scenario.totalPassed}:* ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total /> <br>
</#if>
<#if statistic.failed != 0 >
*${phrases.scenario.totalFailed}:* ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total /> <br>
</#if>
<#if statistic.broken != 0 >
*${phrases.scenario.totalBroken}:* ${statistic.broken} <br>
</#if>
<#if statistic.unknown != 0 >
*${phrases.scenario.totalUnknown}:* ${statistic.unknown} <br>
</#if>
<#if statistic.skipped != 0 >
*${phrases.scenario.totalSkipped}:* ${statistic.skipped} <br>
</#if>

<#if suitesSummaryJson??>
<#assign suitesData = suitesSummaryJson?eval_json>
*${phrases.numberOfSuites}:* ${suitesData.total} <br>
<#list suitesData.items as suite>
<#assign suitePassed = suite.statistic.passed>
<#assign suiteFailed = suite.statistic.failed>
<#assign suiteBroken = suite.statistic.broken>
<#assign suiteUnknown = suite.statistic.unknown>
<#assign suiteSkipped = suite.statistic.skipped>

*${phrases.suiteName}:* ${suite.name}
> *${phrases.scenario.totalScenarios}:* ${suite.statistic.total} <br>
<#if suitePassed != 0 >> *${phrases.scenario.totalPassed}:* ${suitePassed} <br></#if>
<#if suiteFailed != 0 >> *${phrases.scenario.totalFailed}:* ${suiteFailed} <br></#if>
<#if suiteBroken != 0 >> *${phrases.scenario.totalBroken}:* ${suiteBroken} <br></#if>
<#if suiteUnknown != 0 >> *${phrases.scenario.totalUnknown}:* ${suiteUnknown} <br></#if>
<#if suiteSkipped != 0 >> *${phrases.scenario.totalSkipped}:* ${suiteSkipped} <br></#if>
</#list>
</#if>
<#if reportLink??>*${phrases.reportAvailableAtLink}:* ${reportLink} <br></#if>
<#if chartSource??>${chartSource}</#if> <br>
</#compress>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package guru.qa.allure.notifications.clients.gitlab;

import guru.qa.allure.notifications.clients.ClientFactory;
import guru.qa.allure.notifications.clients.Notifier;
import guru.qa.allure.notifications.config.Config;
import guru.qa.allure.notifications.config.gitlab.Gitlab;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;




class ClientFactoryGitlabTest {

@Test
void shouldCreateGitlabClientIfEnabled() {
Config config = new Config();
Gitlab gitlabConfig = buildGitlabConfig(true);
config.setGitlab(gitlabConfig);

List<Notifier> notifiers = ClientFactory.from(config);

assertEquals(1, notifiers.size(), "Only GitlabClient expected");
assertInstanceOf(GitlabClient.class, notifiers.get(0));
}


@Test
void shouldNotCreateGitlabClientWhenConfigIsDisabled() {
Config config = new Config();
Gitlab gitlabConfig = buildGitlabConfig(false);
config.setGitlab(gitlabConfig);
List<Notifier> notifiers = ClientFactory.from(config);

assertTrue(notifiers.isEmpty(), "No clients expected when Gitlab config is disabled");
}

@Test
void shouldNotCreateGitlabClientWhenConfigNull() {
Config config = new Config();

List<Notifier> notifiers = ClientFactory.from(config);

assertTrue(notifiers.isEmpty(), "No clients expected when Gitlab config is absent");
}


private Gitlab buildGitlabConfig(Boolean enabled) {
Gitlab gitlab = new Gitlab();
gitlab.setEnabled(enabled);
gitlab.setUrl("https://gitlab.example.com");
gitlab.setApiKey("apiKey");
gitlab.setApiToken("apiToken");
gitlab.setProjectId("123");
gitlab.setMergeRequestIid("1");
return gitlab;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package guru.qa.allure.notifications.clients.gitlab;

import guru.qa.allure.notifications.config.gitlab.Gitlab;
import guru.qa.allure.notifications.model.phrases.Phrases;
import guru.qa.allure.notifications.model.phrases.Scenario;
import guru.qa.allure.notifications.model.summary.Statistic;
import guru.qa.allure.notifications.template.data.MessageData;
import kong.unirest.*;
import kong.unirest.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class GitlabClientTest {
private GitlabClient gitlabClient;
private Gitlab gitlab;

@Mock private MessageData messageData;
@Mock private HttpRequestWithBody sendRequest;
@Mock private RequestBodyEntity requestBodyEntity;
@Mock private HttpResponse<String> httpResponseString;
@Mock private HttpRequestWithBody uploadRequest;
@Mock private HttpResponse<JsonNode> uploadResponseJson;
@Mock private MultipartBody multipartBody;
@Mock private JsonNode uploadJsonNode;

@BeforeEach
void setUp() throws Exception {
gitlab = buildGitlabConfig();
gitlabClient = new GitlabClient(gitlab);
doReturn(prepareMessageValues()).when(messageData).getValues();

when(sendRequest.routeParam(anyString(), anyString())).thenReturn(sendRequest);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any*** matchers do not provide enough confident quiality level

when(sendRequest.header(anyString(), anyString())).thenReturn(sendRequest);
when(sendRequest.body(any(Map.class))).thenReturn(requestBodyEntity);
when(requestBodyEntity.asString()).thenReturn(httpResponseString);
when(httpResponseString.getBody()).thenReturn("ok");
}

@Test
void sendText_shouldNotThrow() {
try (MockedStatic<Unirest> unirest = Mockito.mockStatic(Unirest.class)) {
unirest.when(() -> Unirest.post(contains("/notes"))).thenReturn(sendRequest);
assertDoesNotThrow(() -> gitlabClient.sendText(messageData));
}
}

@Test
void sendPhoto_shouldNotThrow() {
try (MockedStatic<Unirest> unirest = Mockito.mockStatic(Unirest.class)) {
unirest.when(() -> Unirest.post(contains("/notes"))).thenReturn(sendRequest);
unirest.when(() -> Unirest.post(contains("/uploads"))).thenReturn(uploadRequest);
when(uploadRequest.routeParam(anyString(), anyString())).thenReturn(uploadRequest);
when(uploadRequest.header(anyString(), anyString())).thenReturn(uploadRequest);
when(uploadRequest.field(anyString(), any(), any(), anyString())).thenReturn(multipartBody);
when(multipartBody.asJson()).thenReturn(uploadResponseJson);
when(uploadResponseJson.getBody()).thenReturn(uploadJsonNode);
when(uploadJsonNode.getObject()).thenReturn(new JSONObject());
assertDoesNotThrow(() -> gitlabClient.sendPhoto(messageData, new byte[]{1, 2, 3}));
}
}


private Map<String, Object> prepareMessageValues() throws Exception {
Statistic stat = new Statistic();
setField(stat, "passed", 10);
setField(stat, "failed", 2);
setField(stat, "broken", 1);
setField(stat, "skipped", 3);
setField(stat, "unknown", 0);
setField(stat, "total", 16);

Scenario scenario = new Scenario();
setField(scenario, "duration", "duration");
setField(scenario, "totalScenarios", "totalScenarios");
setField(scenario, "totalPassed", "totalPassed");
setField(scenario, "totalFailed", "totalFailed");
setField(scenario, "totalBroken", "totalBroken");
setField(scenario, "totalUnknown", "totalUnknown");
setField(scenario, "totalSkipped", "totalSkipped");

Phrases phrases = new Phrases();
setField(phrases, "results", "results");
setField(phrases, "environment", "environment");
setField(phrases, "comment", "comment");
setField(phrases, "reportAvailableAtLink", "reportAvailableAtLink");
setField(phrases, "scenario", scenario);
setField(phrases, "numberOfSuites", "numberOfSuites");
setField(phrases, "suiteName", "suiteName");

Map<String, Object> map = new HashMap<>();
map.put("environment", "environment");
map.put("comment", "comment");
map.put("reportLink", "reportLink");
map.put("customData", null);
map.put("time", "time");
map.put("statistic", stat);
map.put("suitesSummaryJson", null);
map.put("phrases", phrases);
return map;

}

private void setField(Object target, String name, Object value) throws Exception {
Field f = target.getClass().getDeclaredField(name);
f.setAccessible(true);
f.set(target, value);
}

private Gitlab buildGitlabConfig() {
Gitlab gitlab = new Gitlab();
gitlab.setUrl("https://gitlab.example.com");
gitlab.setApiKey("apiKey");
gitlab.setApiToken("apiToken");
gitlab.setProjectId("123");
gitlab.setMergeRequestIid("1");
return gitlab;
}

}
Loading