-
Notifications
You must be signed in to change notification settings - Fork 90
Add gitlab merge request note decoration #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
[](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: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳 | ||
|
||
|
@@ -141,6 +141,15 @@ Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳 | |
"port": 0, | ||
"username": "", | ||
"password": "" | ||
}, | ||
"gitlab": { | ||
"enabled": true, | ||
"url": "", | ||
"apiKey": "", | ||
"apiToken": "", | ||
"projectId": "", | ||
"mergeRequestIid": "", | ||
"templatePath": "/templates/gitlab.ftl" | ||
} | ||
} | ||
``` | ||
|
@@ -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> | ||
<li><code>apiKey</code> - имя пользователя для аутентификации</li> | ||
<li><code>apiToken</code> - токен для аутентификации</li> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add the same information to |
||
</details> | ||
|
||
|
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 |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package guru.qa.allure.notifications.config.gitlab; | ||
|
||
import lombok.Data; | ||
|
||
@Data | ||
public class Gitlab { | ||
private Boolean enabled = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<#import "utils.ftl" as utils> | ||
<#compress> | ||
*${phrases.results}:* <br> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the difference between |
||
*${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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add an example