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
5 changes: 5 additions & 0 deletions api-testing-bot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ dependencies {
implementation "com.googlecode.json-simple:json-simple:1.1.1"
implementation "com.konghq:unirest-java:3.13.10"
implementation "i5:las2peer-api-test-model:0.1.9"

// GitHub API
implementation "org.kohsuke:github-api:1.306"
implementation "io.jsonwebtoken:jjwt-impl:0.11.5"
implementation "io.jsonwebtoken:jjwt-jackson:0.11.5"
}

configurations {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public class APITestingBot extends RESTService {
private String botManagerURL;
private String caeBackendURL;

/**
* Id of GitHub app that the bot uses.
*/
private int gitHubAppId;

/**
* Private key of GitHub app that the bot uses.
*/
private String gitHubAppPrivateKey;

public APITestingBot() {
setFieldValues();
}
Expand Down Expand Up @@ -63,7 +73,7 @@ public void sendAPIDocChangesMessage(String openAPIDocOld, String openAPIDocUpda
* @param webhookPayload Payload of the webhook call
* @return JSONObject that can be used as the content of a monitoring message to trigger a webhook call.
*/
private JSONObject createWebhook(String url, JSONObject webhookPayload) {
public static JSONObject createWebhook(String url, JSONObject webhookPayload) {
JSONObject webhook = new JSONObject();
webhook.put("url", url);
webhook.put("payload", webhookPayload);
Expand All @@ -78,7 +88,7 @@ private JSONObject createWebhook(String url, JSONObject webhookPayload) {
* @param channel Channel to which the message should be posted.
* @return JSONObject representing the payload for a webhook call to the SBF that will trigger a chat message.
*/
private JSONObject createWebhookPayload(String message, String messenger, String channel) {
public static JSONObject createWebhookPayload(String message, String messenger, String channel) {
JSONObject webhookPayload = new JSONObject();
webhookPayload.put("event", "chat_message");
webhookPayload.put("message", message);
Expand All @@ -90,4 +100,16 @@ private JSONObject createWebhookPayload(String message, String messenger, String
public String getCaeBackendURL() {
return caeBackendURL;
}

public String getBotManagerURL() {
return botManagerURL;
}

public int getGitHubAppId() {
return gitHubAppId;
}

public String getGitHubAppPrivateKey() {
return gitHubAppPrivateKey;
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package i5.las2peer.services.apiTestingBot;

import i5.las2peer.api.Context;
import i5.las2peer.apiTestModel.*;
import i5.las2peer.services.apiTestingBot.chat.GHMessageHandler;
import i5.las2peer.services.apiTestingBot.chat.Intent;
import i5.las2peer.services.apiTestingBot.chat.MessageHandler;
import i5.las2peer.services.apiTestingBot.chat.RCMessageHandler;
import i5.las2peer.services.apiTestingBot.context.MessengerType;
import i5.las2peer.services.apiTestingBot.context.TestModelingContext;
import i5.las2peer.services.apiTestingBot.context.TestModelingState;
import i5.las2peer.services.apiTestingBot.util.PRTestGenHelper;
import io.swagger.annotations.Api;
import kong.unirest.Unirest;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;

import java.io.Serializable;
import java.net.HttpURLConnection;

import static i5.las2peer.services.apiTestingBot.context.MessengerType.*;
import static i5.las2peer.services.apiTestingBot.context.TestModelingState.*;
Expand Down Expand Up @@ -243,4 +245,84 @@ public Response modelTest(String body) {

return Response.status(200).entity(res.toJSONString()).build();
}

/**
* Gets called by SBF if user comments "@testingbot code".
*
* @param body
* @return Response message containing the generated code.
*/
@POST
@Path("/test/code")
public Response generateTestCode(String body) {
String responseMessage = "";

JSONObject bodyJSON = (JSONObject) JSONValue.parse(body);
String message = (String) bodyJSON.get("msg");
String messenger = (String) bodyJSON.get("messenger");
MessengerType messengerType = MessengerType.fromString(messenger);
String channel = (String) bodyJSON.get("channel");

if(message.equalsIgnoreCase("@testingbot code") && messengerType == GITHUB_PR &&
PRTestGenHelper.generatedTestCases.containsKey(channel)) {
TestCase generatedTestCase = PRTestGenHelper.generatedTestCases.get(channel);

// generate code for the test case
try {
String code = (String) Context.get().invoke("i5.las2peer.services.codeGenerationService.CodeGenerationService",
"generateTestMethod", new Serializable[]{ generatedTestCase });

// post generated code as a comment
responseMessage += "Here is the generated test method code:";
responseMessage += "\n";
responseMessage += "```java\n";
responseMessage += code;
responseMessage += "```";
} catch (Exception e) {
e.printStackTrace();
}
}

JSONObject res = new JSONObject();
res.put("text", responseMessage);

return Response.status(200).entity(res.toJSONString()).build();
}

/**
* Endpoint that receives the webhook events from the GitHub app.
* The events are redirected to the SBF (e.g., to react to issue or PR comments).
* If the event is a pull request workflow run event, it is also checked if the API testing bot can generate
* a test case for an operation that has been added or changed within the pull request.
*
* @param body Event payload
* @param eventName Name of GitHub event
* @param gitHubAppId Id of GitHub app
* @return 200
*/
@POST
@Path("/github/webhook/{gitHubAppId}")
public Response receiveWebhookEvent(String body, @HeaderParam("X-GitHub-Event") String eventName,
@PathParam("gitHubAppId") int gitHubAppId) {
APITestingBot service = (APITestingBot) Context.get().getService();
if(service.getGitHubAppId() != gitHubAppId) return Response.status(HttpURLConnection.HTTP_INTERNAL_ERROR).build();

// redirect event to SBF
this.redirectWebhookEventToSBF(gitHubAppId, eventName, body);

JSONObject jsonBody = (JSONObject) JSONValue.parse(body);
if(PRTestGenHelper.isRelevantWorkflowEvent(eventName, jsonBody)) {
PRTestGenHelper.handleWorkflowEvent(jsonBody, service.getBotManagerURL(), service.getGitHubAppId(),
service.getGitHubAppPrivateKey());
}

return Response.status(HttpURLConnection.HTTP_OK).build();
}

private void redirectWebhookEventToSBF(int gitHubAppId, String eventName, String body) {
APITestingBot service = (APITestingBot) Context.get().getService();
String sbfWebhookUrl = service.getBotManagerURL() + "/github/webhook/" + gitHubAppId;
Unirest.post(sbfWebhookUrl).body(body).header("X-GitHub-Event", eventName).asEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package i5.las2peer.services.apiTestingBot.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.kohsuke.github.GHAppInstallation;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;

import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;

/**
* A GitHub app can be installed multiple times (e.g., within different organizations or repositories).
* To use the GitHub API for an app installation, we need an access token for this app installation.
* For requesting this access token, a JWT is needed. This JWT allows to authenticate as a GitHub app.
* The JWT needs to be signed using the app's private key (from general app settings).
*
* See https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps
*/
public class GitHubAppHelper {

/**
* Id of the GitHub app.
*/
private int gitHubAppId;

/**
* Private key used to sign JWTs.
*/
private Key privateKey;

/**
*
* @param gitHubAppId Id of the GitHub app
* @param pkcs8PrivateKey Private key of GitHub app (already needs to be converted to pkcs8)
* @throws GitHubAppHelperException
*/
public GitHubAppHelper(int gitHubAppId, String pkcs8PrivateKey) throws GitHubAppHelperException {
this.gitHubAppId = gitHubAppId;

byte[] pkcs8PrivateKeyBytes = DatatypeConverter.parseBase64Binary(pkcs8PrivateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8PrivateKeyBytes);
try {
this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new GitHubAppHelperException(e.getMessage());
}
}

/**
* Returns a GitHub object that has access to the given repository.
* @param repositoryFullName Full name of the repository, containing both owner and repository name.
* @return GitHub object that has access to the given repository.
*/
public GitHub getGitHubInstance(String repositoryFullName) {
String ownerName = repositoryFullName.split("/")[0];
String repoName = repositoryFullName.split("/")[1];

try {
// first create GitHub object using a JWT (this is needed to request an access token for an app installation)
GitHub gitHub = new GitHubBuilder().withJwtToken(generateJWT()).build();

// get app installation for given repository (getInstallationByRepository requires a JWT)
GHAppInstallation installation = gitHub.getApp().getInstallationByRepository(ownerName, repoName);

// create a GitHub object with app installation token
return new GitHubBuilder().withAppInstallationToken(installation.createToken().create().getToken()).build();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

/**
* Generates a JWT and signs it with the app's private key.
* @return JWT
*/
private String generateJWT() {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
Date expiration = new Date(nowMillis + 60000);
return Jwts.builder()
.setIssuedAt(now) // issue now
.setExpiration(expiration) // expiration time of JWT
.setIssuer(String.valueOf(gitHubAppId)) // app id needs to be used as issuer
.signWith(this.privateKey, SignatureAlgorithm.RS256) // sign with app's private key
.compact();
}

/**
* General exception that is thrown if something related to the GitHubAppHelper is not working.
*/
public class GitHubAppHelperException extends Exception {
public GitHubAppHelperException(String message) {
super(message);
}
}

}
Loading