Skip to content

Commit

Permalink
Merge pull request #99 from veepee-oss/feature/jira-tags
Browse files Browse the repository at this point in the history
Feature/jira tags
  • Loading branch information
josefd8 authored Jul 13, 2021
2 parents c5b0f13 + 70df537 commit a91e3c6
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 2 deletions.
62 changes: 62 additions & 0 deletions src/main/java/com/privalia/qa/aspects/JiraTagAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.privalia.qa.aspects;

import com.privalia.qa.utils.JiraConnector;
import io.cucumber.testng.FeatureWrapper;
import io.cucumber.testng.PickleWrapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Aspect for managing the @jira() tag on a feature/scenario
*/
@Aspect
public class JiraTagAspect {

private final Logger logger = LoggerFactory.getLogger(this.getClass().getCanonicalName());

JiraConnector jc = new JiraConnector();

/**
* Pointcut is executed for {@link io.cucumber.testng.AbstractTestNGCucumberTests#runScenario(PickleWrapper, FeatureWrapper)}
* @param pickleWrapper the pickleWrapper
* @param featureWrapper the featureWrapper
*/
@Pointcut("execution (void *.runScenario(..)) && args(pickleWrapper, featureWrapper)")
protected void jiraTagPointcutScenario(PickleWrapper pickleWrapper, FeatureWrapper featureWrapper) {
}

@Around(value = "jiraTagPointcutScenario(pickleWrapper, featureWrapper)")
public void aroundJiraTagPointcut(ProceedingJoinPoint pjp, PickleWrapper pickleWrapper, FeatureWrapper featureWrapper) throws Throwable {

List<String> tags = pickleWrapper.getPickle().getTags();
String scenarioName = pickleWrapper.getPickle().getName();

String ticket = this.jc.getFirstTicketReference(tags);

if (ticket != null) {
try {
if (!jc.entityShouldRun(ticket)) {
logger.warn("Scenario '" + scenarioName + "' was ignored, it is in a non runnable status in Jira.");
return;
} else {
pjp.proceed();
}
} catch (Exception e) {
logger.warn("Could not retrieve info of ticket " + ticket + " from jira: " + e.getMessage() + ". Proceeding with execution...");
pjp.proceed();
}

} else {
pjp.proceed();
}
}

}
29 changes: 27 additions & 2 deletions src/main/java/com/privalia/qa/specs/HookGSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.privalia.qa.utils.JiraConnector;
import com.privalia.qa.utils.ThreadProperty;
import io.appium.java_client.MobileDriver;
import io.appium.java_client.android.AndroidDriver;
Expand Down Expand Up @@ -51,6 +52,9 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
Expand All @@ -77,6 +81,7 @@ public class HookGSpec extends BaseGSpec {

private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(HookGSpec.class);

JiraConnector jiraConnector = new JiraConnector();

protected WebDriver driver;

Expand Down Expand Up @@ -472,10 +477,29 @@ public void restClientTeardown() {
}

/**
* Disconnect any remaining open SSH connection after each scenario is completed
* Will check if the scenario contains any reference to a Jira ticket and will try to update
* its status based on the result of the scenario execution. It will also try to close any remaining
* SSH connection
* @param scenario Scenario
*/
@After(order = 10)
public void remoteSSHConnectionTeardown() {
public void teardown(Scenario scenario) {

if (scenario.isFailed()) {
Collection<String> tags = scenario.getSourceTagNames();
String ticket = this.jiraConnector.getFirstTicketReference(new ArrayList(tags));

if (ticket != null) {
try {
commonspec.getLogger().debug("Updating ticket " + ticket + " in Jira...");
this.jiraConnector.transitionEntity(ticket);
this.jiraConnector.postCommentToEntity(ticket, "Scenario '" + scenario.getName() + "' failed at: " + scenario.getId());
} catch (Exception e) {
commonspec.getLogger().warn("Could not change the status of entity " + ticket + " in jira: " + e.getMessage());
}
}
}

if (commonspec.getRemoteSSHConnection() != null) {
commonspec.getLogger().debug("Closing SSH remote connection");
commonspec.getRemoteSSHConnection().getSession().disconnect();
Expand All @@ -494,4 +518,5 @@ public void sqlConnectionClose() throws Exception {
commonspec.getSqlClient().disconnect();
}
}

}
228 changes: 228 additions & 0 deletions src/main/java/com/privalia/qa/utils/JiraConnector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package com.privalia.qa.utils;

import com.jayway.jsonpath.JsonPath;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilder;
import com.ning.http.client.Response;
import com.privalia.qa.lookups.DefaultLookUp;
import org.apache.commons.text.StringSubstitutor;
import net.minidev.json.JSONArray;
import org.apache.commons.text.lookup.StringLookupFactory;

import java.util.List;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* An small utility for interacting with entities in Jira
* @author José Fernández
*/
public class JiraConnector {

public static final String JIRA_PROPERTIES_FILE = "jira.properties";

final StringSubstitutor interpolator = StringSubstitutor.createInterpolator();

AsyncHttpClient client = new AsyncHttpClient();

/**
* Reads the given key from the properties file. The system will try to locate the key first in
* the maven variables (System.getProperty) and if not found will look for it in the properties file.
* If the value is still not found it will return the default value (if provided) or an exception
* @param property key
* @return value
*/
private String getProperty(String property, String defaultValue) {

if (System.getProperty(property) != null) {
return System.getProperty(property);
}

interpolator.setEnableUndefinedVariableException(true);

if (defaultValue != null) {
property = property + ":-" + defaultValue;
}

return interpolator.replace("${properties:src/test/resources/" + JIRA_PROPERTIES_FILE + "::" + property + "}");
}

/**
* Retrieves the current entity status
* @param entity Entity identifier (i.e QMS-123)
* @return Status as string (i.e 'In Progress')
* @throws Exception Exception
*/
private String getEntityStatus(String entity) throws Exception {

String jiraURL = this.getProperty("jira.server.url", null);
String jiraToken = this.getProperty("jira.personal.access.token", null);

Request getRequest = new RequestBuilder()
.setMethod("GET")
.setUrl(jiraURL + "/rest/api/2/issue/" + entity)
.addHeader("Authorization", "Bearer " + jiraToken)
.build();

Future f = this.client.executeRequest(getRequest);
Response r = (Response) f.get();

if (r.getStatusCode() != 200) {
throw new Exception("Unexpected status code response:" + r.getStatusCode() + ". Body: '" + r.getResponseBody() + "'");
}

return JsonPath.read(r.getResponseBody(), "$.fields.status.name").toString().toUpperCase();
}

/**
* Determines if the entity status matches any of the expected statuses
* @param entity Entity identifier (i.e QMS-123)
* @return True if the entity status is within the expected statuses
* @throws Exception Exception
*/
public Boolean entityShouldRun(String entity) throws Exception {

String[] valid_statuses = this.getProperty("jira.valid.runnable.statuses", "Done,Deployed").split(",");
String entity_current_status = this.getEntityStatus(entity).toUpperCase();

for (String status: valid_statuses) {
if (entity_current_status.matches(status.toUpperCase())) {
return true;
}
}

return false;
}

/**
* Change the status of an entity to the Given Status by name. The status name should match exactly a valid
* status for that entity
* @param entity Entity identifier (i.e QMS-123)
* @param new_status New status (i.e 'Done')
* @throws Exception Exception
*/
private void transitionEntityToGivenStatus(String entity, String new_status) throws Exception {

int targetTransition = this.getTransitionIDForEntityByName(entity, new_status);

String jiraURL = this.getProperty("jira.server.url", "");
String jiraToken = this.getProperty("jira.personal.access.token", "");

Request postRequest = new RequestBuilder()
.setMethod("POST")
.setUrl(jiraURL + "/rest/api/2/issue/" + entity + "/transitions")
.addHeader("Authorization", "Bearer " + jiraToken)
.addHeader("Content-Type", "application/json")
.setBody("{\"transition\": {\"id\": " + targetTransition + " }}")
.build();

Future f = this.client.executeRequest(postRequest);
Response r = (Response) f.get();

if (r.getStatusCode() != 204) {
throw new Exception("Unexpected status code response:" + r.getStatusCode() + ". Body: '" + r.getResponseBody() + "'");
}

}

/**
* Gets the id of the transition by the given name
* @param entity Entity identifier (i.e QMS-123)
* @param transitionName Transition name (i.e 'In Progress')
* @return Id of the transition for that name
* @throws Exception Exception
*/
private int getTransitionIDForEntityByName(String entity, String transitionName) throws Exception {

String jiraURL = this.getProperty("jira.server.url", "");
String jiraToken = this.getProperty("jira.personal.access.token", "");

Request getRequest = new RequestBuilder()
.setMethod("GET")
.setUrl(jiraURL + "/rest/api/2/issue/" + entity + "/transitions")
.addHeader("Authorization", "Bearer " + jiraToken)
.build();

Future f = this.client.executeRequest(getRequest);
Response r = (Response) f.get();

if (r.getStatusCode() != 200) {
throw new Exception("Unexpected status code response:" + r.getStatusCode() + ". Body: '" + r.getResponseBody() + "'");
}

Object transitionStrings = JsonPath.read(r.getResponseBody(), "$.transitions[?(@.name=='" + transitionName + "')].id");
JSONArray ja = (JSONArray) transitionStrings;

if (ja.isEmpty()) {
throw new IndexOutOfBoundsException("Could not find the transition '" + transitionName + "' in the list of valid transitions for entity '" + entity + "'");
} else {
return Integer.valueOf(ja.get(0).toString());
}

}

/**
* Adds a new comment to the entity
* @param entity Entity identifier (i.e QMS-123)
* @param message Message to post
* @throws Exception Exception
*/
public void postCommentToEntity(String entity, String message) throws Exception {

String jiraURL = this.getProperty("jira.server.url", "");
String jiraToken = this.getProperty("jira.personal.access.token", "");

Request postRequest = new RequestBuilder()
.setMethod("POST")
.setUrl(jiraURL + "/rest/api/2/issue/" + entity + "/comment")
.addHeader("Authorization", "Bearer " + jiraToken)
.addHeader("Content-Type", "application/json")
.setBody("{\"body\": \"" + message + "\"}")
.build();

Future f = this.client.executeRequest(postRequest);
Response r = (Response) f.get();

if (r.getStatusCode() != 201) {
throw new Exception("Unexpected status code response:" + r.getStatusCode() + ". Body: '" + r.getResponseBody() + "'");
}
}

/**
* Transition (change status) of the entity to the value provided in the properties file. Will
* default to "In Progress" is the value is not found
* @param entity Entity identifier (i.e QMS-123)
* @throws Exception Exception
*/
public void transitionEntity(String entity) throws Exception {

String jiraTransitionToStatus = this.getProperty("jira.transition.if.fail.status", "TO REVIEW");
Boolean jiraTransition = Boolean.valueOf(this.getProperty("jira.transition.if.fail", "true"));

if (jiraTransition) {
this.transitionEntityToGivenStatus(entity, jiraTransitionToStatus);
}

}

/**
* Returns the first reference to the jira ticket from the tag reference
* @param tags List of thats (i.e @ignore, @jira(QMS-123))
* @return The first ticket reference (i.e QMS-123)
*/
public String getFirstTicketReference(List<String> tags) {
String pattern = "@jira\\((.*)\\)";
Pattern r = Pattern.compile(pattern);

for (String tag: tags) {
Matcher m = r.matcher(tag);
if (m.find()) {
return m.group(1);
}
}

return null;
}
}
12 changes: 12 additions & 0 deletions src/test/java/com/privalia/qa/ATests/JiraTagIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.privalia.qa.ATests;

import com.privalia.qa.utils.BaseGTest;
import io.cucumber.testng.CucumberOptions;

@CucumberOptions(
features = {
"src/test/resources/features/jiraTag.feature",
},
glue = "com.privalia.qa.specs")
public class JiraTagIT extends BaseGTest {
}
Loading

0 comments on commit a91e3c6

Please sign in to comment.