diff --git a/config/mariadb/initdb.d/init.sql b/config/mariadb/initdb.d/init.sql index 2a6abea6219dc978d7561e8eb845a69a6505154a..45938283d5c8f57751c83219404ca36206f9670e 100644 --- a/config/mariadb/initdb.d/init.sql +++ b/config/mariadb/initdb.d/init.sql @@ -12,11 +12,13 @@ CREATE TABLE IF NOT EXISTS `CommitValidationMessage` ( CREATE TABLE IF NOT EXISTS `CommitValidationStatus` ( `id` SERIAL, `commitHash` varchar(100) NOT NULL, + `userMail` varchar(100) NOT NULL, `project` varchar(100) NOT NULL, `lastModified` datetime DEFAULT NULL, `creationDate` datetime DEFAULT NULL, `provider` varchar(100) NOT NULL, `repoUrl` varchar(255) DEFAULT NULL, + `entimatedLoc` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ); diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java b/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java index adef078e3b23ad57069dc625cfece3b864770f21..b5b7bdacc995c52269baee97cf92221970dd0fcc 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java +++ b/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java @@ -124,4 +124,8 @@ public class EclipseQuteTemplateExtensions { } return out; } + + private EclipseQuteTemplateExtensions() { + + } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java index 1b4a7cfc7ddaaa281e487e8b1eba04ea3d3ae5f5..2bb409ed17a947de9e719bd9fc05eb6a9aca9686 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java +++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java @@ -11,6 +11,7 @@ **********************************************************************/ package org.eclipsefoundation.git.eca.dto; +import java.io.Serializable; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -38,7 +39,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; @Table @Entity -public class CommitValidationMessage extends BareNode { +public class CommitValidationMessage extends BareNode implements Serializable{ public static final DtoTable TABLE = new DtoTable(CommitValidationMessage.class, "cvm"); @Id diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java index 3fd168db02f562e8972c389950d032ce419ea720..a903c7090e166c01f3d66a0aa5c3d73c596f5205 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java +++ b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java @@ -45,10 +45,10 @@ public class JwtHelper { @ConfigProperty(name = "smallrye.jwt.sign.key.location") String location; @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28") - String apiVersion; + protected String apiVersion; @RestClient - GithubAPI ghApi; + protected GithubAPI ghApi; @Inject JWTParser parser; diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java b/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java index 47361178b52b529a5f2c63c2f20f264ab246846d..bb740946330fc3d75575823760e52251213d8027 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java @@ -14,6 +14,8 @@ package org.eclipsefoundation.git.eca.model; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; + import org.eclipsefoundation.git.eca.namespace.APIStatusCode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -76,6 +78,7 @@ public abstract class CommitStatus { public abstract static class CommitStatusMessage { public abstract APIStatusCode getCode(); + @Nullable public abstract String getMessage(); public static Builder builder() { @@ -87,7 +90,7 @@ public abstract class CommitStatus { public abstract static class Builder { public abstract Builder setCode(APIStatusCode code); - public abstract Builder setMessage(String message); + public abstract Builder setMessage(@Nullable String message); public abstract CommitStatusMessage build(); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java index e1e44475a6fb8e9697d28a1e57e78d4701e50fe6..5d1d70710e96eaa92e00e9c6d0f96fc5a83ee380 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java @@ -40,6 +40,7 @@ public abstract class ValidationRequest { public abstract List<Commit> getCommits(); + @Nullable public abstract ProviderType getProvider(); @Nullable @@ -49,6 +50,8 @@ public abstract class ValidationRequest { @JsonProperty("strictMode") public abstract Boolean getStrictMode(); + public abstract Builder toBuilder(); + public static Builder builder() { return new AutoValue_ValidationRequest.Builder().setStrictMode(false).setCommits(new ArrayList<>()); } @@ -61,7 +64,7 @@ public abstract class ValidationRequest { public abstract Builder setCommits(List<Commit> commits); - public abstract Builder setProvider(ProviderType provider); + public abstract Builder setProvider(@Nullable ProviderType provider); public abstract Builder setEstimatedLoc(@Nullable Integer estimatedLoc); diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java index 0ba5ad5cf8e192fef657984827c63dcceda80636..60a38dc484db54368b2c90a9a7a32625b4e5b936 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java @@ -15,6 +15,7 @@ import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -46,6 +47,7 @@ public abstract class ValidationResponse { public abstract boolean getStrictMode(); + @Nullable public abstract String getFingerprint(); public boolean getPassed() { @@ -130,7 +132,7 @@ public abstract class ValidationResponse { public abstract Builder setStrictMode(boolean strictMode); - public abstract Builder setFingerprint(String fingerprint); + public abstract Builder setFingerprint(@Nullable String fingerprint); public abstract ValidationResponse build(); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/WebhookHeaders.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/WebhookHeaders.java new file mode 100644 index 0000000000000000000000000000000000000000..017f47c47ea7502b4cfe94a31499bc46e9bf1257 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/WebhookHeaders.java @@ -0,0 +1,27 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.git.eca.namespace; + +/** + * A Helper class used to centralize the incoming webhook header values +*/ +public final class WebhookHeaders { + + public static final String GITLAB_EVENT = "X-Gitlab-Event"; + + public static final String GITHUB_EVENT = "X-GitHub-Event"; + public static final String GITHUB_DELIVERY = "X-GitHub-Delivery"; + + private WebhookHeaders() { + + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index a4569876f97c4d9078eb8f8c56c626d248117d66..f02a4015b2c7a0ca52dccc5431185caefd30ab62 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -33,6 +33,7 @@ import org.eclipsefoundation.git.eca.model.RevalidationResponse; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; import org.eclipsefoundation.git.eca.namespace.HCaptchaErrorCodes; +import org.eclipsefoundation.git.eca.namespace.WebhookHeaders; import org.jboss.resteasy.annotations.jaxrs.HeaderParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,8 +64,8 @@ public class GithubWebhooksResource extends GithubAdjacentResource { * @return an OK status when done processing. */ @POST - public Response processGithubWebhook(@HeaderParam("X-GitHub-Delivery") String deliveryId, - @HeaderParam("X-GitHub-Hook-ID") String hookId, @HeaderParam("X-GitHub-Event") String eventType, GithubWebhookRequest request) { + public Response processGithubWebhook(@HeaderParam(WebhookHeaders.GITHUB_DELIVERY) String deliveryId, + @HeaderParam(WebhookHeaders.GITHUB_EVENT) String eventType, GithubWebhookRequest request) { // If event isn't a pr event, drop as we don't process them if (!"pull_request".equalsIgnoreCase(eventType)) { return Response.ok().build(); diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java index 8c612938e0e53d23b617d0dce85f31af71ba696f..8fd1b44f975684f587066fbad69bf7b836aaafbd 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java @@ -83,7 +83,7 @@ public class StatusResource extends GithubAdjacentResource { public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) { List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint); if (statuses.isEmpty()) { - return Response.status(404).build(); + throw new NotFoundException(String.format("Fingerprint '%s' not found", fingerprint)); } List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider()); return Response diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java index 36eb596b693a4ffc0dd85e751f02c2a8116d7ea9..9d4f0c5ff6a4cd9cbeaaac52e237ed0e41dc2799 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.models.EclipseUser; @@ -117,7 +118,7 @@ public class ValidationResource { messages.add("A commit is required to validate"); } // check that we have a repo set - if (req.getRepoUrl() == null) { + if (req.getRepoUrl() == null || StringUtils.isBlank(req.getRepoUrl().getPath())) { messages.add("A base repo URL needs to be set in order to validate"); } // check that we have a type set diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java index a9fd5b266d9eae8d1544371f00cef23fa701fcc7..fc70b304dd5d8c05823386fa68991e7fb2c0ad63 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java @@ -19,6 +19,7 @@ import javax.ws.rs.core.Response; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.git.eca.api.models.SystemHook; import org.eclipsefoundation.git.eca.namespace.EventType; +import org.eclipsefoundation.git.eca.namespace.WebhookHeaders; import org.eclipsefoundation.git.eca.service.SystemHookService; import org.jboss.resteasy.annotations.jaxrs.HeaderParam; import org.slf4j.Logger; @@ -41,7 +42,7 @@ public class WebhooksResource { @POST @Path("system") - public Response processGitlabHook(@HeaderParam("X-Gitlab-Event") String eventHeader, String jsonBody) { + public Response processGitlabHook(@HeaderParam(WebhookHeaders.GITLAB_EVENT) String eventHeader, String jsonBody) { // Do not process if header is incorrect if (!"system hook".equalsIgnoreCase(eventHeader)) { return Response.ok().build(); diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/BadRequestMapper.java b/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/BadRequestMapper.java deleted file mode 100644 index 7a276b81af1c1d94f3457ef9212f51180b2edd67..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/BadRequestMapper.java +++ /dev/null @@ -1,37 +0,0 @@ -/********************************************************************* -* Copyright (c) 2022 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.git.eca.resource.mapper; - -import javax.ws.rs.BadRequestException; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import org.eclipsefoundation.core.model.Error; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Creates human legible error responses in the case of BadRequestExceptions. - */ -@Provider -public class BadRequestMapper implements ExceptionMapper<BadRequestException> { - - private static final Logger LOGGER = LoggerFactory.getLogger(BadRequestMapper.class); - - @Override - public Response toResponse(BadRequestException exception) { - LOGGER.error(exception.getMessage()); - return new Error(Status.BAD_REQUEST, exception.getMessage()).asResponse(); - } -} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/RuntimeMapper.java b/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/RuntimeMapper.java deleted file mode 100644 index 39c317dbe7bb8b7bb9e97ec64331bbde9dbf87f6..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/mapper/RuntimeMapper.java +++ /dev/null @@ -1,41 +0,0 @@ -/********************************************************************* -* Copyright (c) 2019 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Martin Lowe <martin.lowe@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.git.eca.resource.mapper; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Catch-all exception mapper to ensure that any error thrown by the service - * will log the error and quit out safely while limiting the response to a - * simple error. - * - * @author Martin Lowe - * - */ -@Provider -public class RuntimeMapper implements ExceptionMapper<RuntimeException> { - private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeMapper.class); - - @Override - public Response toResponse(RuntimeException exception) { - LOGGER.error(exception.getMessage(), exception); - // return an empty response with a server error response - return Response.status(Status.INTERNAL_SERVER_ERROR).build(); - } - -} diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java index b26a3fb815105bcd64ab33f3b4330fc74ddae726..ff23076451d0c2ec7e6eddf2cdfe369825adf06a 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java @@ -15,6 +15,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; +import org.eclipsefoundation.core.exception.ApplicationException; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.efservices.api.models.Project; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; @@ -98,7 +99,7 @@ public interface ValidationService { try { return HexConverter.convertToHexString(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())); } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Error while encoding request fingerprint - couldn't find MD5 algorithm."); + throw new ApplicationException("Error while encoding request fingerprint - couldn't find MD5 algorithm.", e); } } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java index 9027ca48dbe1a08ad8472ea7ee1ed563d41df7b0..0875d375771e62c7392c333220efc39fc448eb58 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.MultivaluedMap; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.context.ManagedExecutor; import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.core.exception.ApplicationException; import org.eclipsefoundation.core.service.APIMiddleware; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.GithubAPI; @@ -94,7 +95,7 @@ public class DefaultGithubApplicationService implements GithubApplicationService Thread.currentThread().interrupt(); } catch (Exception e) { // rewrap exception and throw - throw new RuntimeException(e); + throw new ApplicationException("Thread interrupted while building repository cache, no entries will be available for current call", e); } return new MultivaluedMapImpl<>(); } diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html index 358cefb5d70336fab754feb06d6adbe388cc468e..8b861a0c59b5da313eb9951e976d15a88c6715d6 100644 --- a/src/main/resources/templates/simple_fingerprint_ui.html +++ b/src/main/resources/templates/simple_fingerprint_ui.html @@ -74,7 +74,7 @@ </li> {/if} </ul> - <a href="{repoUrl}" target="_blank" class="btn btn-primary margin-top-10">Project repository</a> + <a href="{repoUrl}" target="_blank" rel="noopener" class="btn btn-primary margin-top-10">Project repository</a> </div> </div> {#if statuses.getErrorCount > 0} @@ -153,7 +153,7 @@ <div> <form id="git-eca-hook-revalidation" data-request-number="{pullRequestNumber}" data-request-repo="{fullRepoName}" data-request-installation="{installationId}"> <div class="captcha"> - <div class="h-captcha" data-sitekey="{config:['eclipse.hcaptcha.sitekey']}"></div> + <div class="h-captcha" data-sitekey="{config:['eclipse.hcaptcha.site-key']}"></div> </div> <button class="btn-success margin-top-10 btn form-submit" type="submit">Revalidate</button> </form> @@ -161,7 +161,7 @@ {/if} </section> - <aside id="main-sidebar-secondy" role="complementary" class="col-md-6 col-sm-8 margin-bottom-60"> + <aside id="main-sidebar-secondy" role="complementary" class="col-md-6 col-sm-8 margin-bottom-60" aria-label="eclipse-contributor-agreement"> <div class="region region-sidebar-second solstice-region-element-count-2"> <section id="block-site-login-eclipse-eca-sle-eca-lookup-tool" class="block block-site-login-eclipse-eca block-region-sidebar-second solstice-block solstice-block-default solstice-block-white-bg block-sle-eca-lookup-tool clearfix"> @@ -181,7 +181,7 @@ <section id="block-eclipse-api-github-eclipse-api-github-links" class="block block-eclipse-api-github contextual-links-region block-region-sidebar-second solstice-block block-eclipse-api-github-links clearfix"> <div class="block-content"> - <aside class="main-sidebar-default-margin" id="main-sidebar"> + <aside class="main-sidebar-default-margin" id="main-sidebar" aria-label="eclipse-contributor-agreement-links"> <ul id="leftnav" class="ul-left-nav fa-ul hidden-print"> <li class="separator"><a class="separator" href="https://www.eclipse.org/legal/ECA.php"> ECA </a></li> <li><i class="fa fa-caret-right fa-fw" aria-hidden="true"></i> <a href="https://accounts.eclipse.org/user/eca" diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..45ff97b3c39d9430712a8becb3a09e7aad2f5bc5 --- /dev/null +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResourceTest.java @@ -0,0 +1,79 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.git.eca.resource; + +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; + +import org.eclipsefoundation.core.exception.ApplicationException; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.Installation; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequestHead; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.Repository; +import org.eclipsefoundation.git.eca.namespace.WebhookHeaders; +import org.eclipsefoundation.testing.helpers.TestCaseHelper; +import org.eclipsefoundation.testing.models.EndpointTestBuilder; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class GithubWebhooksResourceTest { + private static final String GH_WEBHOOK_BASE_URL = "/webhooks/github"; + + @Inject + ObjectMapper om; + + @Test + void testInvalidEventType() { + EndpointTestBuilder.from(TestCaseHelper.prepareTestCase(GH_WEBHOOK_BASE_URL, new String[] {}, null) + .setHeaderParams(Optional.of(Map.of(WebhookHeaders.GITHUB_EVENT, "nope"))).build()).doPost() + .run(); + } + + @Test + void testGHWebhook_success() { + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(GH_WEBHOOK_BASE_URL, new String[] {}, null) + .setHeaderParams(Optional.of(Map.of(WebhookHeaders.GITHUB_DELIVERY, "id-1", WebhookHeaders.GITHUB_EVENT, "pull_request"))) + .build()) + .doPost(createGHWebhook()) + .run(); + } + + private String createGHWebhook() { + try { + return om.writeValueAsString(GithubWebhookRequest.builder() + .setInstallation(Installation.builder().setId("install-id").build()) + .setPullRequest(PullRequest + .builder() + .setNumber(42) + .setHead(PullRequestHead.builder().setSha("sha-1234").build()) + .setState("open") + .build()) + .setRepository(Repository + .builder() + .setFullName("eclipsefdn/sample") + .setHtmlUrl("http://www.github.com/eclipsefdn/sample") + .build()) + .build()); + } catch (Exception e) { + throw new ApplicationException("Error converting Hook to JSON"); + } + } +} diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java index e329ce2f3377814f7846b100a3fc55afdd682e1b..8def7dcf42cb21cacafe2cbd42a365eb1537380a 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java @@ -97,16 +97,6 @@ class ReportsResourceTest { RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_SUCCESS_CASE); } - @Test - void getPrivProjReport_failure_invalidResponseFormat() { - RestAssuredTemplates - .testGet(TestCaseHelper - .prepareTestCase(REPORTS_PROJECTS_URL, new String[] { VALID_TEST_ACCESS_KEY }, null) - .setResponseContentType(ContentType.TEXT) - .setStatusCode(500) - .build()); - } - @Test void getPrivProjReport_failure_badAccessKey() { RestAssuredTemplates.testGet(GET_REPORT_BAD_ACCESS_KEY); diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/StatusResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/StatusResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f6e850d2799d4acb7c52644a9066bd74cb1b6714 --- /dev/null +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/StatusResourceTest.java @@ -0,0 +1,78 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.git.eca.resource; + +import java.util.Collections; +import java.util.Map; + +import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper; +import org.eclipsefoundation.testing.helpers.TestCaseHelper; +import org.eclipsefoundation.testing.models.EndpointTestBuilder; +import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; + +@QuarkusTest +class StatusResourceTest { + public static final String STATUS_BASE_URL = "/eca/status"; + public static final String FINGERPRINT_STATUS_BASE_URL = STATUS_BASE_URL + "/{fingerprint}"; + public static final String FINGERPRINT_STATUS_UI_BASE_URL = FINGERPRINT_STATUS_BASE_URL + "/ui"; + + private static final String VALID_FINGERPRINT = "957706b0f31e0ccfc5287c0ebc62dc79"; + private static final String INVALID_FINGERPRINT = "nope"; + + public static final EndpointTestCase GET_STATUS_SUCCESS_CASE = TestCaseHelper + .buildSuccessCase(FINGERPRINT_STATUS_BASE_URL, new String[] { VALID_FINGERPRINT }, + SchemaNamespaceHelper.COMMIT_VALIDATION_STATUSES_SCHEMA); + + public static final EndpointTestCase GET_STATUS_UI_NOT_FOUND_CASE = TestCaseHelper + .prepareTestCase(FINGERPRINT_STATUS_UI_BASE_URL, new String[] { INVALID_FINGERPRINT }, null) + .setResponseContentType(ContentType.HTML) + .setStatusCode(404).build(); + + @Test + void testGetValidationStatus_success_validFormatAndSchema() { + EndpointTestBuilder.from(GET_STATUS_SUCCESS_CASE).doGet().andCheckSchema().andCheckFormat().run(); + } + + @Test + void testGetValidationStatus_success_invalidFingerprint() { + // invalid/not found fingerprint returns 200 with an empty list + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(FINGERPRINT_STATUS_BASE_URL, new String[] { INVALID_FINGERPRINT }, null) + .setBodyValidationParams(Map.of("", Collections.emptyList())).build()) + .andCheckBodyParams() + .doGet() + .run(); + } + + @Test + void testGetValidationStatusUi_success() { + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(FINGERPRINT_STATUS_UI_BASE_URL, new String[] { VALID_FINGERPRINT }, null) + .setResponseContentType(ContentType.HTML).build()) + .doGet().run(); + } + + @Test + void testGetValidationStatusUi_failure_notFound() { + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(FINGERPRINT_STATUS_UI_BASE_URL, new String[] { INVALID_FINGERPRINT }, null) + .setResponseContentType(ContentType.HTML).setStatusCode(404).build()) + .doGet().run(); + } +} diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java index 1c2a56a055a9acecaadc3c79491193ac2346fd8c..ba99ddc39d06d1a1f94acf00cc3bd5af1f8b23bf 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java @@ -25,6 +25,7 @@ import java.util.UUID; import javax.inject.Inject; +import org.eclipsefoundation.core.exception.ApplicationException; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.model.Commit; import org.eclipsefoundation.git.eca.model.GitUser; @@ -151,7 +152,7 @@ class ValidationResourceTest { try { in = json.writeValueAsString(VALIDATE_SUCCESS_BODY); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new ApplicationException("Error converting body to JSON format", e); } Assertions.assertTrue(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.VALIDATION_REQUEST_SCHEMA_PATH).matches(in)); @@ -423,6 +424,59 @@ class ValidationResourceTest { createGitHubRequest(true, "http://www.github.com/eclipsefdn-tck/tck-ignored", Arrays.asList(c1))); } + @Test + void testValidate_success_noCommits() { + + // We do not block contributions to non-project repos + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, + createGitHubRequest(true, "http://www.github.com/eclipsefdn/prototype.git", + Collections.emptyList())); + + // Strictmode shouldn't affect the result + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, + createGitHubRequest(false, "http://www.github.com/eclipsefdn/prototype.git", + Collections.emptyList())); + } + + @Test + void testValidate_failure_noRepoUrl() { + Commit c1 = createStandardUsercommit(USER_WIZARD, USER_NEWBIE); + + // We do not block contributions to non-project repos + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, createGitHubRequest(true, "", Arrays.asList(c1))); + + // Strictmode shouldn't affect the result + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, + createGitHubRequest(false, "", Arrays.asList(c1))); + } + + @Test + void testValidate_failure_noProvider() { + Commit c1 = createStandardUsercommit(USER_WIZARD, USER_NEWBIE); + + // Request with no provider set + ValidationRequest req = ValidationRequest + .builder() + .setStrictMode(true) + .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/prototype.git")) + .setCommits(Arrays.asList(c1)) + .build(); + + // We do not block contributions to non-project repos + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, req); + + req = req.toBuilder().setStrictMode(false).build(); + + // Strictmode shouldn't affect the result + RestAssuredTemplates + .testPost(VALIDATE_SUCCESS_CASE, req); + } + /* * BOT ACCESS TESTS */ diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/WebhooksResourceTest.java similarity index 85% rename from src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java rename to src/test/java/org/eclipsefoundation/git/eca/resource/WebhooksResourceTest.java index 23a1d312f08ed6993510273de208699eef1c70fa..857e80d80916c7fa264b03652c1458e1f1148b74 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/WebhooksResourceTest.java @@ -16,6 +16,9 @@ import java.util.Arrays; import java.util.Map; import java.util.Optional; +import javax.inject.Inject; + +import org.eclipsefoundation.core.exception.ApplicationException; import org.eclipsefoundation.git.eca.api.models.SystemHook; import org.eclipsefoundation.git.eca.api.models.SystemHook.Owner; import org.eclipsefoundation.testing.helpers.TestCaseHelper; @@ -23,10 +26,12 @@ import org.eclipsefoundation.testing.templates.RestAssuredTemplates; import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.quarkus.test.junit.QuarkusTest; @QuarkusTest -class WebhoooksResourceTest { +class WebhooksResourceTest { public static final String WEBHOOKS_BASE_URL = "/webhooks"; public static final String GITLAB_HOOK_URL = WEBHOOKS_BASE_URL + "/gitlab/system"; @@ -124,33 +129,44 @@ class WebhoooksResourceTest { public static final EndpointTestCase CASE_HOOK_MISSING_HEADER = TestCaseHelper.buildSuccessCase( GITLAB_HOOK_URL, new String[] {}, null); + @Inject + ObjectMapper om; + @Test void processCreateHook_success() { - RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, PROJECT_CREATE_HOOK_VALID); + RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, convertHookToJson(PROJECT_CREATE_HOOK_VALID)); } @Test void processDeleteHook_success() { - RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, PROJECT_DESTROY_HOOK_VALID); + RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, convertHookToJson(PROJECT_DESTROY_HOOK_VALID)); } @Test void processRenameHook_success() { - RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, PROJECT_RENAME_HOOK_VALID); + RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, convertHookToJson(PROJECT_RENAME_HOOK_VALID)); } @Test void processCreateHook_success_untrackedEvent() { - RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, HOOK_NOT_TRACKED); + RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, convertHookToJson(HOOK_NOT_TRACKED)); } @Test void processCreateHook_success_missingEventName() { - RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, HOOK_MISSING_EVENT); + RestAssuredTemplates.testPost(CASE_HOOK_SUCCESS, convertHookToJson(HOOK_MISSING_EVENT)); } @Test void processCreateHook_success_missingHeaderParam() { - RestAssuredTemplates.testPost(CASE_HOOK_MISSING_HEADER, PROJECT_CREATE_HOOK_VALID); + RestAssuredTemplates.testPost(CASE_HOOK_MISSING_HEADER, convertHookToJson(PROJECT_CREATE_HOOK_VALID)); + } + + private String convertHookToJson(SystemHook hook) { + try { + return om.writeValueAsString(hook); + } catch (Exception e) { + throw new ApplicationException("Error converting Hook to JSON"); + } } } diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGithubAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGithubAPI.java new file mode 100644 index 0000000000000000000000000000000000000000..441d15d6b65292664fa1460dae8de0c556d9563e --- /dev/null +++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGithubAPI.java @@ -0,0 +1,106 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.git.eca.test.api; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters; +import org.eclipsefoundation.git.eca.api.GithubAPI; +import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; +import org.eclipsefoundation.git.eca.api.models.GithubCommit; +import org.eclipsefoundation.git.eca.api.models.GithubCommit.CommitData; +import org.eclipsefoundation.git.eca.api.models.GithubCommit.GitCommitUser; +import org.eclipsefoundation.git.eca.api.models.GithubCommit.GithubCommitUser; +import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; + +import io.quarkus.test.Mock; + +@Mock +@RestClient +@ApplicationScoped +public class MockGithubAPI implements GithubAPI { + + Map<String, Map<Integer, List<GithubCommit>>> commits; + Map<String, Map<String, String>> commitStatuses; + + public MockGithubAPI() { + this.commitStatuses = new HashMap<>(); + this.commits = new HashMap<>(); + this.commits.put("eclipsefdn/sample", + Map.of(42, + Arrays.asList(GithubCommit.builder() + .setSha("sha-1234") + .setAuthor(GithubCommitUser.builder().setLogin("testuser").build()) + .setCommitter(GithubCommitUser.builder().setLogin("testuser").build()) + .setParents(Collections.emptyList()) + .setCommit(CommitData.builder() + .setAuthor( + GitCommitUser.builder().setName("The Wizard") + .setEmail("code.wiz@important.co") + .build()) + .setCommitter( + GitCommitUser.builder().setName("The Wizard") + .setEmail("code.wiz@important.co") + .build()) + .build()) + .build()))); + } + + @Override + public PullRequest getPullRequest(String bearer, String apiVersion, String repoFull, int pullNumber) { + return PullRequest.builder().build(); + } + + @Override + public Response getCommits(String bearer, String apiVersion, String repoFull, int pullNumber) { + List<GithubCommit> results = commits.get(repoFull).get(pullNumber); + if (results == null && !results.isEmpty()) { + return Response.status(404).build(); + } + return Response.ok(results).build(); + } + + @Override + public Response updateStatus(String bearer, String apiVersion, String repoFull, String prHeadSha, + GithubCommitStatusRequest commitStatusUpdate) { + commitStatuses.computeIfAbsent(repoFull, m -> new HashMap<>()).merge(prHeadSha, commitStatusUpdate.getState(), + (k, v) -> v); + return Response.ok().build(); + } + + @Override + public Response getInstallations(BaseAPIParameters params, String bearer) { + throw new UnsupportedOperationException("Unimplemented method 'getInstallations'"); + } + + @Override + public GithubAccessToken getNewAccessToken(String bearer, String apiVersion, String installationId) { + return GithubAccessToken.builder() + .setToken("gh-token-" + installationId) + .setExpiresAt(LocalDateTime.now().plusHours(1L)).build(); + } + + @Override + public Response getInstallationRepositories(BaseAPIParameters params, String bearer) { + throw new UnsupportedOperationException("Unimplemented method 'getInstallationRepositories'"); + } +} diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGitlabAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGitlabAPI.java index 4062fa8b826b0301cd9de1abeec44ce01b9a1b3c..1252307f7caa1b63b875050409cd6c447470114a 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGitlabAPI.java +++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockGitlabAPI.java @@ -11,6 +11,7 @@ **********************************************************************/ package org.eclipsefoundation.git.eca.test.api; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,16 +42,22 @@ public class MockGitlabAPI implements GitlabAPI { GitlabProjectResponse.builder() .setId(69) .setCreatorId(1) + .setPathWithNamespace("namespace/path") + .setCreatedAt(ZonedDateTime.now()) .build(), GitlabProjectResponse.builder() .setId(42) .setCreatorId(55) .setForkedFromProject(ForkedProject.builder().setId(41).build()) + .setPathWithNamespace("namespace/path") + .setCreatedAt(ZonedDateTime.now()) .build(), GitlabProjectResponse.builder() .setId(95) .setCreatorId(33) .setForkedFromProject(ForkedProject.builder().setId(69).build()) + .setPathWithNamespace("namespace/path") + .setCreatedAt(ZonedDateTime.now()) .build())); users = new ArrayList<>(); diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/helper/TestJWTHelper.java b/src/test/java/org/eclipsefoundation/git/eca/test/helper/TestJWTHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..369f93cbb48a8e6b480dd42693320253918d9912 --- /dev/null +++ b/src/test/java/org/eclipsefoundation/git/eca/test/helper/TestJWTHelper.java @@ -0,0 +1,33 @@ +package org.eclipsefoundation.git.eca.test.helper; + +import javax.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.git.eca.api.GithubAPI; +import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; +import org.eclipsefoundation.git.eca.helper.JwtHelper; + +import io.quarkus.cache.CacheResult; +import io.quarkus.test.Mock; + +@Singleton +@Mock +public class TestJWTHelper extends JwtHelper { + + @Override + public String getGhBearerString(String installationId) { + return "Bearer " + getGithubAccessToken(installationId).getToken(); + } + + @CacheResult(cacheName = "accesstoken") + @Override + public GithubAccessToken getGithubAccessToken(String installationId) { + return ghApi.getNewAccessToken("Bearer " + generateJwt(), apiVersion, installationId); + } + + @Override + public String generateJwt() { + return "jwt-test"; + } +} diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java b/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java index 6bf874f9d757abd6baec2af53ecff7d31c05bf48..838fc9bf55a42c06757a6299dd0697ebdf51c649 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java +++ b/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java @@ -25,5 +25,10 @@ public final class SchemaNamespaceHelper { public static final String PRIVATE_PROJECT_EVENTS_SCHEMA_PATH = BASE_SCHEMAS_PATH + "private-project-events" + BASE_SCHEMAS_PATH_SUFFIX; + public static final String COMMIT_VALIDATION_STATUS_SCHEMA = BASE_SCHEMAS_PATH + "commit-validation-status" + + BASE_SCHEMAS_PATH_SUFFIX; + public static final String COMMIT_VALIDATION_STATUSES_SCHEMA = BASE_SCHEMAS_PATH + "commit-validation-statuses" + + BASE_SCHEMAS_PATH_SUFFIX; + public static final String ERROR_SCHEMA_PATH = BASE_SCHEMAS_PATH + "error" + BASE_SCHEMAS_PATH_SUFFIX; } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index b468fd8f1c70edda794f5b7ff65f270b7ee6d2e6..ed2bc8fb08d1992cd53873ad2ad94b65c3eb3d10 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -36,4 +36,7 @@ eclipse.git-eca.mail.allow-list=noreply@github.com eclipse.git-eca.reports.access-key=samplekey ## Misc -eclipse.gitlab.access-token=token_val \ No newline at end of file +eclipse.gitlab.access-token=token_val + +## Disable private project scan in test mode +eclipse.scheduled.private-project.enabled=false \ No newline at end of file diff --git a/src/test/resources/database/default/V1.0.0__default.sql b/src/test/resources/database/default/V1.0.0__default.sql index 09fa5c3dffe1f162ea9e86bf5c352a5055a9e41f..100590d363360f140356726c5d366cfe12a9e56d 100644 --- a/src/test/resources/database/default/V1.0.0__default.sql +++ b/src/test/resources/database/default/V1.0.0__default.sql @@ -51,7 +51,7 @@ INSERT INTO PrivateProjectEvent(userId, projectId, projectPath, ef_username, par CREATE TABLE GithubWebhookTracking ( id SERIAL NOT NULL, lastKnownState varchar(63) NOT NULL, - deliveryId varchar(63) NOT NULL, + installationId varchar(63) NOT NULL, headSha varchar(63) NOT NULL, repositoryFullName varchar(127) NOT NULL, pullRequestNumber int NOT NULL,