From a32155b5003578f972ee0556a5751e90aca649a0 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Mon, 18 Apr 2022 16:18:19 -0400
Subject: [PATCH 01/10] Add initial DB implementation to store commit
 validation states

Includes a few basic tests for revalidation logic. Adds H2 as test db
and maria is used as default binding. Some way of tracking pr/mr commits
for dashboard view is needed, as right now commits are associated only
by hash and project ID (for basic collision mitigation)
---
 config/sample.secret.properties               |   6 +-
 docker-compose.yaml                           |   1 +
 pom.xml                                       | 402 ++++----
 .../git/eca/dto/CommitValidationMessage.java  |  94 ++
 .../git/eca/dto/CommitValidationStatus.java   | 202 ++++
 .../git/eca/model/ValidationResponse.java     |   5 +-
 .../git/eca/namespace/APIStatusCode.java      |   2 +-
 .../eca/namespace/GitEcaParameterNames.java   |  24 +
 .../git/eca/resource/ValidationResource.java  | 964 +++++++++---------
 src/main/resources/application.properties     |  11 +
 .../eca/resource/ValidationResourceTest.java  | 209 +++-
 .../git/eca/test/dao/TestPersistenceDao.java  |  27 +
 src/test/resources/application.properties     |  12 +-
 .../database/default/V1.0.0__default.sql      |  20 +
 14 files changed, 1287 insertions(+), 692 deletions(-)
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/dao/TestPersistenceDao.java
 create mode 100644 src/test/resources/database/default/V1.0.0__default.sql

diff --git a/config/sample.secret.properties b/config/sample.secret.properties
index 564b90f3..bd69c382 100644
--- a/config/sample.secret.properties
+++ b/config/sample.secret.properties
@@ -1,3 +1,7 @@
 ## Required for authenticated requests to profile API
 oauth2.client-id=sample
-oauth2.client-secret=sample
\ No newline at end of file
+oauth2.client-secret=sample
+
+quarkus.datasource.username = root
+quarkus.datasource.password = eclipse_sample
+quarkus.datasource.jdbc.url=jdbc:mariadb://mariadb/eclipse
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 81dd6f83..724e9a63 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -9,6 +9,7 @@ services:
       VIRTUAL_PORT: 443
       VIRTUAL_PROTO: https
       CERT_NAME: dev.docker
+  shm_size: '256m'
     ports:
       - 443:443
       - 80:80
diff --git a/pom.xml b/pom.xml
index 20561aad..40e7405f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,196 +1,224 @@
 <?xml version="1.0"?>
 <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
-	<modelVersion>4.0.0</modelVersion>
-	<groupId>org.eclipsefoundation</groupId>
-	<artifactId>git-eca</artifactId>
-	<version>0.0.1</version>
-	<properties>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.eclipsefoundation</groupId>
+  <artifactId>git-eca</artifactId>
+  <version>0.0.1</version>
+  <properties>
 		<eclipse-api-version>0.6.3-SNAPSHOT</eclipse-api-version>
-		<compiler-plugin.version>3.8.1</compiler-plugin.version>
-		<maven.compiler.parameters>true</maven.compiler.parameters>
+    <compiler-plugin.version>3.8.1</compiler-plugin.version>
+    <maven.compiler.parameters>true</maven.compiler.parameters>
 		<maven.compiler.source>11</maven.compiler.source>
 		<maven.compiler.target>11</maven.compiler.target>
-		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-		<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
-		<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
-		<quarkus.platform.version>2.6.3.Final</quarkus.platform.version>
-		<surefire-plugin.version>2.22.1</surefire-plugin.version>
-		<auto-value.version>1.8.2</auto-value.version>
-	</properties>
-	<repositories>
-		<repository>
-			<id>eclipsefdn</id>
-			<url>https://repo.eclipse.org/content/repositories/eclipsefdn/</url>
-			<releases>
-				<enabled>true</enabled>
-			</releases>
-			<snapshots>
-				<enabled>true</enabled>
-			</snapshots>
-		</repository>
-	</repositories>
-	<dependencyManagement>
-		<dependencies>
-			<dependency>
-				<groupId>${quarkus.platform.group-id}</groupId>
-				<artifactId>${quarkus.platform.artifact-id}</artifactId>
-				<version>${quarkus.platform.version}</version>
-				<type>pom</type>
-				<scope>import</scope>
-			</dependency>
-		</dependencies>
-	</dependencyManagement>
-	<dependencies>
-		<dependency>
-			<groupId>org.eclipsefoundation</groupId>
-			<artifactId>quarkus-core</artifactId>
-			<version>${eclipse-api-version}</version>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-resteasy</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-resteasy-jackson</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-smallrye-context-propagation</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-rest-client</artifactId>
-		</dependency>
-		<!-- Annotation preprocessors - reduce all of the boiler plate -->
-		<dependency>
-			<groupId>com.google.auto.value</groupId>
-			<artifactId>auto-value</artifactId>
-			<version>${auto-value.version}</version>
-			<scope>provided</scope>
-		</dependency>
-		<dependency>
-			<groupId>com.google.auto.value</groupId>
-			<artifactId>auto-value-annotations</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>com.google.code.findbugs</groupId>
-			<artifactId>jsr305</artifactId>
-		</dependency>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
+    <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
+    <quarkus.platform.version>2.6.3.Final</quarkus.platform.version>
+    <surefire-plugin.version>2.22.1</surefire-plugin.version>
+    <auto-value.version>1.8.2</auto-value.version>
+  </properties>
+  <repositories>
+    <repository>
+      <id>eclipsefdn</id>
+      <url>https://repo.eclipse.org/content/repositories/eclipsefdn/</url>
+      <releases>
+        <enabled>true</enabled>
+      </releases>
+      <snapshots>
+        <enabled>true</enabled>
+      </snapshots>
+    </repository>
+  </repositories>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>${quarkus.platform.artifact-id}</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>org.eclipsefoundation</groupId>
+      <artifactId>quarkus-core</artifactId>
+      <version>${eclipse-api-version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipsefoundation</groupId>
+      <artifactId>quarkus-persistence</artifactId>
+      <version>${eclipse-api-version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-resteasy</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-resteasy-jackson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-smallrye-context-propagation</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-rest-client</artifactId>
+    </dependency>
+    <!-- Annotation preprocessors - reduce all of the boiler plate -->
+    <dependency>
+      <groupId>com.google.auto.value</groupId>
+      <artifactId>auto-value</artifactId>
+      <version>${auto-value.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.auto.value</groupId>
+      <artifactId>auto-value-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
 
-		<!-- Third-party reqs -->
-		<dependency>
-			<groupId>com.github.scribejava</groupId>
-			<artifactId>scribejava-apis</artifactId>
-			<version>6.4.1</version>
-		</dependency>
-		<!-- Caching -->
-		<dependency>
-			<groupId>com.google.guava</groupId>
-			<artifactId>guava</artifactId>
-		</dependency>
+    <!-- Third-party reqs -->
+    <dependency>
+      <groupId>com.github.scribejava</groupId>
+      <artifactId>scribejava-apis</artifactId>
+      <version>6.4.1</version>
+    </dependency>
+    <!-- Caching -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
 
-		<!-- Test requirements -->
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-junit5</artifactId>
-			<scope>test</scope>
-		</dependency>
-		<dependency>
-			<groupId>io.rest-assured</groupId>
-			<artifactId>rest-assured</artifactId>
-			<scope>test</scope>
-		</dependency>
-		<dependency>
-			<groupId>io.rest-assured</groupId>
-			<artifactId>json-schema-validator</artifactId>
-			<scope>test</scope>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-junit5-mockito</artifactId>
-			<scope>test</scope>
-		</dependency>
-	</dependencies>
+    <!-- Test requirements -->
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-junit5</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>rest-assured</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>json-schema-validator</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-junit5-mockito</artifactId>
+      <scope>test</scope>
+    </dependency>
 
-	<build>
-		<plugins>
-			<plugin>
-				<groupId>${quarkus.platform.group-id}</groupId>
-				<artifactId>quarkus-maven-plugin</artifactId>
-				<version>${quarkus.platform.version}</version>
-				<extensions>true</extensions>
-				<executions>
-					<execution>
-						<goals>
-							<goal>build</goal>
-							<goal>generate-code</goal>
-							<goal>generate-code-tests</goal>
-						</goals>
-					</execution>
-				</executions>
-			</plugin>
-			<plugin>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<version>${compiler-plugin.version}</version>
-				<configuration>
-					<annotationProcessorPaths>
-						<path>
-							<groupId>com.google.auto.value</groupId>
-							<artifactId>auto-value</artifactId>
-							<version>${auto-value.version}</version>
-						</path>
-					</annotationProcessorPaths>
-				</configuration>
-			</plugin>
-			<plugin>
-				<artifactId>maven-surefire-plugin</artifactId>
-				<version>${surefire-plugin.version}</version>
-				<configuration>
-					<skipTests>false</skipTests>
-					<systemPropertyVariables>
-						<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
-						<maven.home>${maven.home}</maven.home>
-					</systemPropertyVariables>
-				</configuration>
-			</plugin>
-		</plugins>
-	</build>
-	<profiles>
-		<profile>
-			<id>native</id>
-			<activation>
-				<property>
-					<name>native</name>
-				</property>
-			</activation>
-			<build>
-				<plugins>
-					<plugin>
-						<artifactId>maven-failsafe-plugin</artifactId>
-						<version>${surefire-plugin.version}</version>
-						<executions>
-							<execution>
-								<goals>
-									<goal>integration-test</goal>
-									<goal>verify</goal>
-								</goals>
-								<configuration>
-									<systemPropertyVariables>
-										<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
-										<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
-										<maven.home>${maven.home}</maven.home>
-									</systemPropertyVariables>
-								</configuration>
-							</execution>
-						</executions>
-					</plugin>
-				</plugins>
-			</build>
-			<properties>
-				<quarkus.package.type>native</quarkus.package.type>
-			</properties>
-		</profile>
-	</profiles>
+    <!-- Following H2/devservices deps are made to circumvent need for docker -->
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-devservices-h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-jdbc-h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- Flyway specific dependencies, used to setup tables in test -->
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-flyway</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>quarkus-maven-plugin</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <extensions>true</extensions>
+        <executions>
+          <execution>
+            <goals>
+              <goal>build</goal>
+              <goal>generate-code</goal>
+              <goal>generate-code-tests</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>${compiler-plugin.version}</version>
+        <configuration>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>com.google.auto.value</groupId>
+              <artifactId>auto-value</artifactId>
+              <version>${auto-value.version}</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire-plugin.version}</version>
+        <configuration>
+          <skipTests>false</skipTests>
+          <systemPropertyVariables>
+            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+            <maven.home>${maven.home}</maven.home>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <profiles>
+    <profile>
+      <id>native</id>
+      <activation>
+        <property>
+          <name>native</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${surefire-plugin.version}</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
+                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+                    <maven.home>${maven.home}</maven.home>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <properties>
+        <quarkus.package.type>native</quarkus.package.type>
+      </properties>
+    </profile>
+  </profiles>
 </project>
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
new file mode 100644
index 00000000..70c45b74
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
@@ -0,0 +1,94 @@
+package org.eclipsefoundation.git.eca.dto;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus.CommitValidationStatusCompositeId;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+@Table
+@Entity
+public class CommitValidationMessage extends BareNode {
+    public static final DtoTable TABLE = new DtoTable(CommitValidationMessage.class, "cvm");
+
+    @EmbeddedId
+    private CommitValidationStatusCompositeId compositeId;
+    private String message;
+
+    @Override
+    public Object getId() {
+        return getCompositeId();
+    }
+
+    /**
+     * @return the compositeId
+     */
+    public CommitValidationStatusCompositeId getCompositeId() {
+        return compositeId;
+    }
+
+    /**
+     * @param compositeId the compositeId to set
+     */
+    public void setCompositeId(CommitValidationStatusCompositeId compositeId) {
+        this.compositeId = compositeId;
+    }
+
+    /**
+     * @return the message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * @param message the message to set
+     */
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("CommitValidationMessage [compositeId=");
+        builder.append(compositeId);
+        builder.append(", message=");
+        builder.append(message);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    @Singleton
+    public static class CommitValidationMessageFilter implements DtoFilter<CommitValidationMessage> {
+        @Inject
+        ParameterizedSQLStatementBuilder builder;
+
+        @Override
+        public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
+            ParameterizedSQLStatement stmt = builder.build(TABLE);
+            // sha check
+            String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
+            if (StringUtils.isNumeric(id)) {
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".compositeId.sha = ?",
+                        new Object[] { id }));
+            }
+            return stmt;
+        }
+
+        @Override
+        public Class<CommitValidationMessage> getType() {
+            return CommitValidationMessage.class;
+        }
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
new file mode 100644
index 00000000..ae2554d3
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
@@ -0,0 +1,202 @@
+package org.eclipsefoundation.git.eca.dto;
+
+import java.io.Serializable;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.persistence.Embeddable;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinColumns;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+@Table
+@Entity
+public class CommitValidationStatus extends BareNode {
+    public static final DtoTable TABLE = new DtoTable(CommitValidationStatus.class, "cvs");
+
+    @EmbeddedId
+    private CommitValidationStatusCompositeId compositeId;
+    private int errorCount;
+    private ZonedDateTime creationDate;
+    private ZonedDateTime lastModified;
+    @OneToMany
+    @JoinColumns({ @JoinColumn(name = "sha", referencedColumnName = "sha", updatable = false),
+            @JoinColumn(name = "projectId", referencedColumnName = "projectId", updatable = false) })
+    private List<CommitValidationMessage> messages;
+
+    @Override
+    public Object getId() {
+        return getCompositeId();
+    }
+
+    /**
+     * @return the compositeId
+     */
+    public CommitValidationStatusCompositeId getCompositeId() {
+        return compositeId;
+    }
+
+    /**
+     * @param compositeId the compositeId to set
+     */
+    public void setCompositeId(CommitValidationStatusCompositeId compositeId) {
+        this.compositeId = compositeId;
+    }
+
+    /**
+     * @return the errorCount
+     */
+    public int getErrorCount() {
+        return errorCount;
+    }
+
+    /**
+     * @param errorCount the errorCount to set
+     */
+    public void setErrorCount(int errorCount) {
+        this.errorCount = errorCount;
+    }
+
+    /**
+     * @return the creationDate
+     */
+    public ZonedDateTime getCreationDate() {
+        return creationDate;
+    }
+
+    /**
+     * @param creationDate the creationDate to set
+     */
+    public void setCreationDate(ZonedDateTime creationDate) {
+        this.creationDate = creationDate;
+    }
+
+    /**
+     * @return the lastModified
+     */
+    public ZonedDateTime getLastModified() {
+        return lastModified;
+    }
+
+    /**
+     * @param lastModified the lastModified to set
+     */
+    public void setLastModified(ZonedDateTime lastModified) {
+        this.lastModified = lastModified;
+    }
+
+    /**
+     * @return the messages
+     */
+    public List<CommitValidationMessage> getMessages() {
+        return messages;
+    }
+
+    /**
+     * @param messages the messages to set
+     */
+    public void setMessages(List<CommitValidationMessage> messages) {
+        this.messages = messages;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("CommitValidationStatus [compositeId=");
+        builder.append(compositeId);
+        builder.append(", errorCount=");
+        builder.append(errorCount);
+        builder.append(", creationDate=");
+        builder.append(creationDate);
+        builder.append(", lastModified=");
+        builder.append(lastModified);
+        builder.append(", messages=");
+        builder.append(messages);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    @Embeddable
+    public static class CommitValidationStatusCompositeId implements Serializable {
+        private static final long serialVersionUID = 1L;
+        private String sha;
+        private String projectId;
+
+        /**
+         * @return the sha
+         */
+        public String getSha() {
+            return sha;
+        }
+
+        /**
+         * @param sha the sha to set
+         */
+        public void setSha(String sha) {
+            this.sha = sha;
+        }
+
+        /**
+         * @return the projectId
+         */
+        public String getProjectId() {
+            return projectId;
+        }
+
+        /**
+         * @param projectId the projectId to set
+         */
+        public void setProjectId(String projectId) {
+            this.projectId = projectId;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("CommitValidationStatusCompositeId [sha=");
+            builder.append(sha);
+            builder.append(", projectId=");
+            builder.append(projectId);
+            builder.append("]");
+            return builder.toString();
+        }
+
+    }
+
+    @Singleton
+    public static class CommitValidationStatusFilter implements DtoFilter<CommitValidationStatus> {
+        @Inject
+        ParameterizedSQLStatementBuilder builder;
+
+        @Override
+        public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
+            ParameterizedSQLStatement stmt = builder.build(TABLE);
+            // sha check
+            String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
+            if (StringUtils.isNumeric(id)) {
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".compositeId.sha = ?",
+                        new Object[] { id }));
+            }
+            return stmt;
+        }
+
+        @Override
+        public Class<CommitValidationStatus> getType() {
+            return CommitValidationStatus.class;
+        }
+    }
+}
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 1071df85..f6c0b0f3 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
@@ -35,6 +35,7 @@ import com.google.auto.value.extension.memoized.Memoized;
 @JsonNaming(LowerCamelCaseStrategy.class)
 @JsonDeserialize(builder = AutoValue_ValidationResponse.Builder.class)
 public abstract class ValidationResponse {
+    public static final String NIL_HASH_PLACEHOLDER = "_nil";
 
     public abstract ZonedDateTime getTime();
 
@@ -68,8 +69,8 @@ public abstract class ValidationResponse {
         getCommits().computeIfAbsent(getHashKey(hash), k -> CommitStatus.builder().build()).addError(error, code);
     }
 
-    private String getHashKey(String hash) {
-        return hash == null ? "_nil" : hash;
+    public static String getHashKey(String hash) {
+        return hash == null ? NIL_HASH_PLACEHOLDER : hash;
     }
 
     /**
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/APIStatusCode.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/APIStatusCode.java
index 220ca9d0..fdf0a309 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/APIStatusCode.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/APIStatusCode.java
@@ -12,7 +12,7 @@ package org.eclipsefoundation.git.eca.namespace;
 import com.fasterxml.jackson.annotation.JsonValue;
 
 public enum APIStatusCode {
-	SUCCESS_DEFAULT(200), SUCCESS_COMMITTER(201), SUCCESS_CONTRIBUTOR(202), ERROR_DEFAULT(-401), ERROR_SIGN_OFF(-402),
+	SUCCESS_DEFAULT(200), SUCCESS_COMMITTER(201), SUCCESS_CONTRIBUTOR(202),SUCCESS_SKIPPED(203), ERROR_DEFAULT(-401), ERROR_SIGN_OFF(-402),
 	ERROR_SPEC_PROJECT(-403), ERROR_AUTHOR(-404), ERROR_COMMITTER(-405), ERROR_PROXY_PUSH(-406);
 
 	private int code;
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
new file mode 100644
index 00000000..d998635d
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -0,0 +1,24 @@
+package org.eclipsefoundation.git.eca.namespace;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Singleton;
+
+import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
+
+@Singleton
+public final class GitEcaParameterNames implements UrlParameterNamespace {
+    public static final String SHA_RAW = "sha";
+    public static final String SHAS_RAW = "shas";
+    public static final String PROJECT_ID_RAW = "project_id";
+    public static final UrlParameter SHA = new UrlParameter(SHA_RAW);
+    public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW);
+    public static final UrlParameter PROJECT_ID = new UrlParameter(PROJECT_ID_RAW);
+
+    @Override
+    public List<UrlParameter> getParameters() {
+        return Arrays.asList(SHA, SHAS, PROJECT_ID);
+    }
+
+}
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 2a14d535..b0023d18 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -24,23 +24,34 @@ import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
+import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus.CommitValidationStatusCompositeId;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.CommitStatus;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
 import org.eclipsefoundation.git.eca.service.ProjectsService;
 import org.eclipsefoundation.git.eca.service.UserService;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.service.FilterService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,13 +59,9 @@ import org.slf4j.LoggerFactory;
 import com.fasterxml.jackson.databind.JsonNode;
 
 /**
- * ECA validation endpoint for Git commits. Will use information from the bots,
- * projects, and
- * accounts API to validate commits passed to this endpoint. Should be as system
- * agnostic as
- * possible to allow for any service to request validation with less reliance on
- * services external
- * to the Eclipse foundation.
+ * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate
+ * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request
+ * validation with less reliance on services external to the Eclipse foundation.
  *
  * @author Martin Lowe
  */
@@ -62,489 +69,528 @@ import com.fasterxml.jackson.databind.JsonNode;
 @Consumes({ MediaType.APPLICATION_JSON })
 @Produces({ MediaType.APPLICATION_JSON })
 public class ValidationResource {
-  private static final Logger LOGGER = LoggerFactory.getLogger(ValidationResource.class);
-
-  @Inject
-  @ConfigProperty(name = "eclipse.mail.allowlist")
-  List<String> allowListUsers;
-
-  // eclipse API rest client interfaces
-  @Inject
-  UserService users;
-  @Inject
-  @RestClient
-  BotsAPI bots;
-
-  // external API/service harnesses
-  @Inject
-  CachingService cache;
-  @Inject
-  ProjectsService projects;
-
-  /**
-   * Consuming a JSON request, this method will validate all passed commits, using
-   * the repo URL and
-   * the repository provider. These commits will be validated to ensure that all
-   * users are covered
-   * either by an ECA, or are committers on the project. In the case of ECA-only
-   * contributors, an
-   * additional sign off footer is required in the body of the commit.
-   *
-   * @param req the request containing basic data plus the commits to be validated
-   * @return a web response indicating success or failure for each commit, along
-   *         with standard
-   *         messages that may be used to give users context on failure.
-   * @throws MalformedURLException
-   */
-  @POST
-  public Response validate(ValidationRequest req) {
-    List<String> messages = checkRequest(req);
-    // only process if we have no errors
-    if (messages.isEmpty()) {
-      LOGGER.debug("Processing: {}", req);
-      // filter the projects based on the repo URL. At least one repo in project must
-      // match the repo URL to be valid
-      List<Project> filteredProjects = retrieveProjectsForRequest(req);
-      ValidationResponse r = ValidationResponse.builder().setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
-              .setTrackedProject(!filteredProjects.isEmpty()).build();
-      for (Commit c : req.getCommits()) {
-        // process the request, capturing if we should continue processing
-        boolean continueProcessing = processCommit(c, r, filteredProjects, req.getProvider());
-        // if there is a reason to stop processing, break the loop
-        if (!continueProcessing) {
-          break;
+    private static final Logger LOGGER = LoggerFactory.getLogger(ValidationResource.class);
+
+    @Inject
+    @ConfigProperty(name = "eclipse.mail.allowlist")
+    List<String> allowListUsers;
+    @Inject
+    @ConfigProperty(name = "eclipse.noreply.email-patterns")
+    List<String> emailPatterns;
+
+    @Inject
+    RequestWrapper wrapper;
+
+    @Inject
+    PersistenceDao dao;
+    @Inject
+    FilterService filters;
+
+    // eclipse API rest client interfaces
+    @Inject
+    UserService users;
+    @Inject
+    @RestClient
+    BotsAPI bots;
+
+    // external API/service harnesses
+    @Inject
+    CachingService cache;
+    @Inject
+    ProjectsService projects;
+
+    /**
+     * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository
+     * provider. These commits will be validated to ensure that all users are covered either by an ECA, or are
+     * committers on the project. In the case of ECA-only contributors, an additional sign off footer is required in the
+     * body of the commit.
+     *
+     * @param req the request containing basic data plus the commits to be validated
+     * @return a web response indicating success or failure for each commit, along with standard messages that may be
+     * used to give users context on failure.
+     * @throws MalformedURLException
+     */
+    @POST
+    public Response validate(ValidationRequest req) {
+        List<String> messages = checkRequest(req);
+        // only process if we have no errors
+        if (messages.isEmpty()) {
+            LOGGER.debug("Processing: {}", req);
+            // filter the projects based on the repo URL. At least one repo in project must
+            // match the repo URL to be valid
+            List<Project> filteredProjects = retrieveProjectsForRequest(req);
+            ValidationResponse r = ValidationResponse.builder()
+                    .setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
+                    .setTrackedProject(!filteredProjects.isEmpty()).build();
+            String projectIdentifier = filteredProjects.isEmpty() ? req.getRepoUrl().toString()
+                    : filteredProjects.get(0).getProjectId();
+            List<CommitValidationStatus> statuses = getCurrentCommitValidationStatus(req, projectIdentifier);
+            for (Commit c : req.getCommits()) {
+                // get the current status if present
+                Optional<CommitValidationStatus> status = statuses.stream()
+                        .filter(s -> c.getHash().equals(s.getCompositeId().getSha())).findFirst();
+                // skip the commit validation if already passed
+                if (status.isPresent() && status.get().getErrorCount() == 0) {
+                    r.addMessage(c.getHash(), "Commit was previously validated, skipping processing",
+                            APIStatusCode.SUCCESS_SKIPPED);
+                    continue;
+                }
+                // process the request, capturing if we should continue processing
+                boolean continueProcessing = processCommit(c, r, filteredProjects, req.getProvider());
+                // update the persistent validation state
+                // if there is a reason to stop processing, break the loop
+                if (!continueProcessing) {
+                    break;
+                }
+            }
+            updateCommitValidationStatus(r, projectIdentifier, statuses);
+            return r.toResponse();
+        } else {
+            // create a stubbed response with the errors
+            ValidationResponse out = ValidationResponse.builder().build();
+            messages.forEach(m -> addError(out, m, null));
+            return out.toResponse();
         }
-      }
-      return r.toResponse();
-    } else {
-        // create a stubbed response with the errors
-        ValidationResponse out = ValidationResponse.builder().build();
-        messages.forEach(m -> addError(out, m, null));
-        return out.toResponse();
     }
-  }
-  
-  /**
-   * Check if there are any issues with the validation request, returning error messages if there are issues with the
-   * request.
-   * 
-   * @param req the current validation request
-   * @return a list of error messages to report, or an empty list if there are no errors with the request.
-   */
-  private List<String> checkRequest(ValidationRequest req) {
-      // check that we have commits to validate
-      List<String> messages = new ArrayList<>();
-      if (req.getCommits() == null || req.getCommits().isEmpty()) {
-          messages.add("A commit is required to validate");
-      }
-      // check that we have a repo set
-      if (req.getRepoUrl() == null) {
-          messages.add("A base repo URL needs to be set in order to validate");
-      }
-      // check that we have a type set
-      if (req.getProvider() == null) {
-          messages.add("A provider needs to be set to validate a request");
-      }
-      return messages;
-  }
-
-  /**
-   * Process the current request, validating that the passed commit is valid. The
-   * author and
-   * committers Eclipse Account is retrieved, which are then used to check if the
-   * current commit is
-   * valid for the current project.
-   *
-   * @param c                the commit to process
-   * @param response         the response container
-   * @param filteredProjects tracked projects for the current request
-   * @return true if we should continue processing, false otherwise.
-   */
-  private boolean processCommit(
-      Commit c,
-      ValidationResponse response,
-      List<Project> filteredProjects,
-      ProviderType provider) {
-    // ensure the commit is valid, and has required fields
-    if (!CommitHelper.validateCommit(c)) {
-      addError(
-          response,
-          "One or more commits were invalid. Please check the payload and try again",
-          c.getHash());
-      return false;
-    }
-    // retrieve the author + committer for the current request
-    GitUser author = c.getAuthor();
-    GitUser committer = c.getCommitter();
-
-    addMessage(response, String.format("Reviewing commit: %1$s", c.getHash()), c.getHash());
-    addMessage(
-        response,
-        String.format("Authored by: %1$s <%2$s>", author.getName(), author.getMail()),
-        c.getHash());
-
-    // skip processing if a merge commit
-    if (c.getParents().size() > 1) {
-      addMessage(
-          response,
-          String.format(
-              "Commit '%1$s' has multiple parents, merge commit detected, passing", c.getHash()),
-          c.getHash());
-      return true;
+
+    private void updateCommitValidationStatus(ValidationResponse r, String projectIdentifier,
+            List<CommitValidationStatus> statuses) {
+        // remove existing messaging
+        RDBMSQuery<CommitValidationMessage> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class),
+                getCommitParams(r, projectIdentifier));
+        // if present, remove any current validation messages as we will be pushing new ones
+        dao.delete(q);
+        List<CommitValidationMessage> messages = new ArrayList<>();
+        List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
+        for (Entry<String, CommitStatus> e : r.getCommits().entrySet()) {
+            if (ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())) {
+                LOGGER.warn("Not logging errors/validation associated with unknown commit");
+                continue;
+            }
+            // if there are errors, update validation messages
+            if (e.getValue().getErrors().size() > 0) {
+                messages.addAll(e.getValue().getErrors().stream().map(err -> {
+                    CommitValidationMessage m = new CommitValidationMessage();
+                    CommitValidationStatusCompositeId id = new CommitValidationStatusCompositeId();
+                    id.setProjectId(projectIdentifier);
+                    id.setSha(e.getKey());
+                    m.setCompositeId(id);
+                    m.setMessage(err.getMessage());
+                    return m;
+                }).collect(Collectors.toList()));
+            }
+            // update the status if present, otherwise make new one.
+            Optional<CommitValidationStatus> status = statuses.stream()
+                    .filter(s -> e.getKey().equals(s.getCompositeId().getSha())).findFirst();
+            CommitValidationStatus base;
+            if (status.isPresent()) {
+                base = status.get();
+            } else {
+                base = new CommitValidationStatus();
+                CommitValidationStatusCompositeId id = new CommitValidationStatusCompositeId();
+                id.setProjectId(projectIdentifier);
+                id.setSha(e.getKey());
+                base.setCompositeId(id);
+                base.setCreationDate(DateTimeHelper.now());
+            }
+            base.setErrorCount(e.getValue().getErrors().size());
+            base.setLastModified(DateTimeHelper.now());
+            updatedStatuses.add(base);
+        }
+        // update the base commit status and messages
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
     }
 
-    // retrieve the eclipse account for the author
-    EclipseUser eclipseAuthor = getIdentifiedUser(author);
-    // if the user is a bot, generate a stubbed user
-    if (isAllowedUser(author.getMail()) || userIsABot(author.getMail(), filteredProjects)) {
-      addMessage(
-          response,
-          String.format(
-              "Automated user '%1$s' detected for author of commit %2$s",
-              author.getMail(), c.getHash()),
-          c.getHash());
-      eclipseAuthor = EclipseUser.createBotStub(author);
-    } else if (eclipseAuthor == null) {
-      addMessage(
-          response,
-          String.format(
-              "Could not find an Eclipse user with mail '%1$s' for author of commit %2$s",
-              author.getMail(), c.getHash()),
-          c.getHash());
-      addError(response, "Author must have an Eclipse Account", c.getHash(), APIStatusCode.ERROR_AUTHOR);
-      return true;
+    private List<CommitValidationStatus> getCurrentCommitValidationStatus(ValidationRequest req,
+            String projectIdentifier) {
+        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
+                getCommitParams(req, projectIdentifier));
+        // set use limit to false to collect all data in one request
+        q.setUseLimit(false);
+        return dao.get(q);
     }
 
-    // retrieve the eclipse account for the committer
-    EclipseUser eclipseCommitter = getIdentifiedUser(committer);
-    // check if whitelisted or bot
-    if (isAllowedUser(committer.getMail()) || userIsABot(committer.getMail(), filteredProjects)) {
-      addMessage(
-          response,
-          String.format(
-              "Automated user '%1$s' detected for committer of commit %2$s",
-              committer.getMail(), c.getHash()),
-          c.getHash());
-      eclipseCommitter = EclipseUser.createBotStub(committer);
-    } else if (eclipseCommitter == null) {
-      addMessage(
-          response,
-          String.format(
-              "Could not find an Eclipse user with mail '%1$s' for committer of commit %2$s",
-              committer.getMail(), c.getHash()),
-          c.getHash());
-      addError(response, "Committing user must have an Eclipse Account", c.getHash(), APIStatusCode.ERROR_COMMITTER);
-      return true;
+    private MultivaluedMap<String, String> getCommitParams(ValidationRequest req, String projectIdentifier) {
+        return getCommitParams(req.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()),
+                projectIdentifier);
     }
-    // validate author access to the current repo
-    validateUserAccess(response, c, eclipseAuthor, filteredProjects, APIStatusCode.ERROR_AUTHOR);
-
-    // check committer general access
-    boolean isCommittingUserCommitter = isCommitter(response, eclipseCommitter, c.getHash(), filteredProjects);
-    validateUserAccessPartial(response, c, eclipseCommitter, isCommittingUserCommitter, APIStatusCode.ERROR_COMMITTER);
-    return true;
-  }
-
-  /**
-   * Validates author access for the current commit. If there are errors, they are
-   * recorded in the
-   * response for the current request to be returned once all validation checks
-   * are completed.
-   *
-   * @param r                the current response object for the request
-   * @param c                the commit that is being validated
-   * @param eclipseUser      the user to validate on a branch
-   * @param filteredProjects tracked projects for the current request
-   * @param errorCode        the error code to display if the user does not have
-   *                         access
-   */
-  private void validateUserAccess(
-      ValidationResponse r,
-      Commit c,
-      EclipseUser eclipseUser,
-      List<Project> filteredProjects, APIStatusCode errorCode) {
-    // call isCommitter inline and pass to partial call
-    validateUserAccessPartial(r, c, eclipseUser, isCommitter(r, eclipseUser, c.getHash(), filteredProjects), errorCode);
-  }
-
-  /**
-   * Allows for isCommitter to be called external to this method. This was
-   * extracted to ensure that isCommitter isn't
-   * called twice for the same user when checking committer proxy push rules and
-   * committer general access.
-   * 
-   * @param r           the current response object for the request
-   * @param c           the commit that is being validated
-   * @param eclipseUser the user to validate on a branch
-   * @param isCommitter the results of the isCommitter call from this class.
-   * @param errorCode   the error code to display if the user does not have access
-   */
-  private void validateUserAccessPartial(ValidationResponse r, Commit c, EclipseUser eclipseUser,
-      boolean isCommitter, APIStatusCode errorCode) {
-    String userType = "author";
-    if (APIStatusCode.ERROR_COMMITTER.equals(errorCode)) {
-      userType = "committer";
+
+    private MultivaluedMap<String, String> getCommitParams(ValidationResponse r, String projectIdentifier) {
+        return getCommitParams(List.copyOf(r.getCommits().keySet()), projectIdentifier);
     }
-    if (isCommitter) {
-      addMessage(r,
-          String.format("Eclipse user '%s'(%s) is a committer on the project.", eclipseUser.getName(), userType),
-          c.getHash());
-    } else {
-      addMessage(r,
-          String.format("Eclipse user '%s'(%s) is not a committer on the project.", eclipseUser.getName(), userType),
-          c.getHash());
-      // check if the author is signed off if not a committer
-      if (eclipseUser.getECA().getSigned()) {
-        addMessage(
-            r,
-            String.format("Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.",
-                eclipseUser.getName(), userType),
-            c.getHash());
-      } else {
-        addMessage(
-            r,
-            String.format("Eclipse user '%s'(%s) does not have a current Eclipse Contributor Agreement (ECA) on file.\n"
-                + "If there are multiple commits, please ensure that each author has a ECA.", eclipseUser.getName(),
-                userType),
-            c.getHash());
-        addError(r,
-            String.format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).",
-                eclipseUser.getName(), userType),
-            c.getHash(), errorCode);
-      }
+
+    private MultivaluedMap<String, String> getCommitParams(List<String> commitShas, String projectIdentifier) {
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.put(GitEcaParameterNames.SHAS_RAW, commitShas);
+        params.add(GitEcaParameterNames.PROJECT_ID_RAW, projectIdentifier);
+        return params;
     }
-  }
-
-  /**
-   * Checks whether the given user is a committer on the project. If they are and
-   * the project is
-   * also a specification for a working group, an additional access check is made
-   * against the user.
-   *
-   * <p>
-   * Additionally, a check is made to see if the user is a registered bot user for
-   * the given
-   * project. If they match for the given project, they are granted committer-like
-   * access to the
-   * repository.
-   *
-   * @param r                the current response object for the request
-   * @param user             the user to validate on a branch
-   * @param hash             the hash of the commit that is being validated
-   * @param filteredProjects tracked projects for the current request
-   * @return true if user is considered a committer, false otherwise.
-   */
-  private boolean isCommitter(
-      ValidationResponse r,
-      EclipseUser user,
-      String hash,
-      List<Project> filteredProjects) {
-    // iterate over filtered projects
-    for (Project p : filteredProjects) {
-      LOGGER.debug("Checking project '{}' for user '{}'", p.getName(), user.getName());
-      // check if any of the committers usernames match the current user
-      if (p.getCommitters().stream().anyMatch(u -> u.getUsername().equals(user.getName()))) {
-        // check if the current project is a committer project, and if the user can
-        // commit to specs
-        if (p.getSpecWorkingGroup() != null && !user.getECA().getCanContributeSpecProject()) {
-          // set error + update response status
-          r.addError(
-              hash,
-              String.format(
-                  "Project is a specification for the working group '%1$s', but user does not have permission to modify a specification project",
-                  p.getSpecWorkingGroup()),
-              APIStatusCode.ERROR_SPEC_PROJECT);
-          return false;
-        } else {
-          LOGGER.debug(
-              "User '{}' was found to be a committer on current project repo '{}'",
-              user.getMail(),
-              p.getName());
-          return true;
+
+    /**
+     * Check if there are any issues with the validation request, returning error messages if there are issues with the
+     * request.
+     * 
+     * @param req the current validation request
+     * @return a list of error messages to report, or an empty list if there are no errors with the request.
+     */
+    private List<String> checkRequest(ValidationRequest req) {
+        // check that we have commits to validate
+        List<String> messages = new ArrayList<>();
+        if (req.getCommits() == null || req.getCommits().isEmpty()) {
+            messages.add("A commit is required to validate");
         }
-      }
-    }
-    // check if user is a bot, either through early detection or through on-demand
-    // check
-    if ((user.getIsBot() != null && user.getIsBot()) || userIsABot(user.getMail(), filteredProjects)) {
-      LOGGER.debug("User '{} <{}>' was found to be a bot", user.getName(), user.getMail());
-      return true;
+        // check that we have a repo set
+        if (req.getRepoUrl() == null) {
+            messages.add("A base repo URL needs to be set in order to validate");
+        }
+        // check that we have a type set
+        if (req.getProvider() == null) {
+            messages.add("A provider needs to be set to validate a request");
+        }
+        return messages;
     }
-    return false;
-  }
 
-  private boolean userIsABot(String mail, List<Project> filteredProjects) {
-    if (mail == null || "".equals(mail.trim())) {
-      return false;
+    /**
+     * Process the current request, validating that the passed commit is valid. The author and committers Eclipse
+     * Account is retrieved, which are then used to check if the current commit is valid for the current project.
+     *
+     * @param c the commit to process
+     * @param response the response container
+     * @param filteredProjects tracked projects for the current request
+     * @return true if we should continue processing, false otherwise.
+     */
+    private boolean processCommit(Commit c, ValidationResponse response, List<Project> filteredProjects,
+            ProviderType provider) {
+        // ensure the commit is valid, and has required fields
+        if (!CommitHelper.validateCommit(c)) {
+            addError(response, "One or more commits were invalid. Please check the payload and try again", c.getHash());
+            return false;
+        }
+        // retrieve the author + committer for the current request
+        GitUser author = c.getAuthor();
+        GitUser committer = c.getCommitter();
+
+        addMessage(response, String.format("Reviewing commit: %1$s", c.getHash()), c.getHash());
+        addMessage(response, String.format("Authored by: %1$s <%2$s>", author.getName(), author.getMail()),
+                c.getHash());
+
+        // skip processing if a merge commit
+        if (c.getParents().size() > 1) {
+            addMessage(response,
+                    String.format("Commit '%1$s' has multiple parents, merge commit detected, passing", c.getHash()),
+                    c.getHash());
+            return true;
+        }
+
+        // retrieve the eclipse account for the author
+        EclipseUser eclipseAuthor = getIdentifiedUser(author);
+        // if the user is a bot, generate a stubbed user
+        if (isAllowedUser(author.getMail()) || userIsABot(author.getMail(), filteredProjects)) {
+            addMessage(response, String.format("Automated user '%1$s' detected for author of commit %2$s",
+                    author.getMail(), c.getHash()), c.getHash());
+            eclipseAuthor = EclipseUser.createBotStub(author);
+        } else if (eclipseAuthor == null) {
+            addMessage(response,
+                    String.format("Could not find an Eclipse user with mail '%1$s' for author of commit %2$s",
+                            author.getMail(), c.getHash()),
+                    c.getHash());
+            addError(response, "Author must have an Eclipse Account", c.getHash(), APIStatusCode.ERROR_AUTHOR);
+            return true;
+        }
+
+        // retrieve the eclipse account for the committer
+        EclipseUser eclipseCommitter = getIdentifiedUser(committer);
+        // check if whitelisted or bot
+        if (isAllowedUser(committer.getMail()) || userIsABot(committer.getMail(), filteredProjects)) {
+            addMessage(response, String.format("Automated user '%1$s' detected for committer of commit %2$s",
+                    committer.getMail(), c.getHash()), c.getHash());
+            eclipseCommitter = EclipseUser.createBotStub(committer);
+        } else if (eclipseCommitter == null) {
+            addMessage(response,
+                    String.format("Could not find an Eclipse user with mail '%1$s' for committer of commit %2$s",
+                            committer.getMail(), c.getHash()),
+                    c.getHash());
+            addError(response, "Committing user must have an Eclipse Account", c.getHash(),
+                    APIStatusCode.ERROR_COMMITTER);
+            return true;
+        }
+        // validate author access to the current repo
+        validateUserAccess(response, c, eclipseAuthor, filteredProjects, APIStatusCode.ERROR_AUTHOR);
+
+        // check committer general access
+        boolean isCommittingUserCommitter = isCommitter(response, eclipseCommitter, c.getHash(), filteredProjects);
+        validateUserAccessPartial(response, c, eclipseCommitter, isCommittingUserCommitter,
+                APIStatusCode.ERROR_COMMITTER);
+        return true;
     }
-    List<JsonNode> botObjs = getBots();
-    // if there are no matching projects, then check against all bots, not just
-    // project bots
-    if (filteredProjects == null || filteredProjects.isEmpty()) {
-      return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
+
+    /**
+     * Validates author access for the current commit. If there are errors, they are recorded in the response for the
+     * current request to be returned once all validation checks are completed.
+     *
+     * @param r the current response object for the request
+     * @param c the commit that is being validated
+     * @param eclipseUser the user to validate on a branch
+     * @param filteredProjects tracked projects for the current request
+     * @param errorCode the error code to display if the user does not have access
+     */
+    private void validateUserAccess(ValidationResponse r, Commit c, EclipseUser eclipseUser,
+            List<Project> filteredProjects, APIStatusCode errorCode) {
+        // call isCommitter inline and pass to partial call
+        validateUserAccessPartial(r, c, eclipseUser, isCommitter(r, eclipseUser, c.getHash(), filteredProjects),
+                errorCode);
     }
-    // for each of the matched projects, check the bot for the matching project ID
-    for (Project p : filteredProjects) {
-      LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
-      for (JsonNode bot : botObjs) {
-        // if the project ID match, and one of the email fields matches, then user is
-        // bot
-        if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
-            && checkFieldsForMatchingMail(bot, mail)) {
-          return true;
+
+    /**
+     * Allows for isCommitter to be called external to this method. This was extracted to ensure that isCommitter isn't
+     * called twice for the same user when checking committer proxy push rules and committer general access.
+     * 
+     * @param r the current response object for the request
+     * @param c the commit that is being validated
+     * @param eclipseUser the user to validate on a branch
+     * @param isCommitter the results of the isCommitter call from this class.
+     * @param errorCode the error code to display if the user does not have access
+     */
+    private void validateUserAccessPartial(ValidationResponse r, Commit c, EclipseUser eclipseUser, boolean isCommitter,
+            APIStatusCode errorCode) {
+        String userType = "author";
+        if (APIStatusCode.ERROR_COMMITTER.equals(errorCode)) {
+            userType = "committer";
+        }
+        if (isCommitter) {
+            addMessage(r, String.format("Eclipse user '%s'(%s) is a committer on the project.", eclipseUser.getName(),
+                    userType), c.getHash());
+        } else {
+            addMessage(r, String.format("Eclipse user '%s'(%s) is not a committer on the project.",
+                    eclipseUser.getName(), userType), c.getHash());
+            // check if the author is signed off if not a committer
+            if (eclipseUser.getECA().getSigned()) {
+                addMessage(r,
+                        String.format(
+                                "Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.",
+                                eclipseUser.getName(), userType),
+                        c.getHash());
+            } else {
+                addMessage(r, String.format(
+                        "Eclipse user '%s'(%s) does not have a current Eclipse Contributor Agreement (ECA) on file.\n"
+                                + "If there are multiple commits, please ensure that each author has a ECA.",
+                        eclipseUser.getName(), userType), c.getHash());
+                addError(r, String.format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).",
+                        eclipseUser.getName(), userType), c.getHash(), errorCode);
+            }
         }
-      }
     }
-    return false;
-  }
-
-  /**
-   * Checks JSON node to look for email fields, both at the root, and nested email
-   * fields.
-   * 
-   * @param bot  the bots JSON object representation
-   * @param mail the email to match against
-   * @return true if the bot has a matching email value, otherwise false
-   */
-  private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
-    // check the root email for the bot for match
-    JsonNode botmail = bot.get("email");
-    if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
-      LOGGER.debug("Found matching bot at root level for '{}'", mail);
-      return true;
+
+    /**
+     * Checks whether the given user is a committer on the project. If they are and the project is also a specification
+     * for a working group, an additional access check is made against the user.
+     *
+     * <p>
+     * Additionally, a check is made to see if the user is a registered bot user for the given project. If they match
+     * for the given project, they are granted committer-like access to the repository.
+     *
+     * @param r the current response object for the request
+     * @param user the user to validate on a branch
+     * @param hash the hash of the commit that is being validated
+     * @param filteredProjects tracked projects for the current request
+     * @return true if user is considered a committer, false otherwise.
+     */
+    private boolean isCommitter(ValidationResponse r, EclipseUser user, String hash, List<Project> filteredProjects) {
+        // iterate over filtered projects
+        for (Project p : filteredProjects) {
+            LOGGER.debug("Checking project '{}' for user '{}'", p.getName(), user.getName());
+            // check if any of the committers usernames match the current user
+            if (p.getCommitters().stream().anyMatch(u -> u.getUsername().equals(user.getName()))) {
+                // check if the current project is a committer project, and if the user can
+                // commit to specs
+                if (p.getSpecWorkingGroup() != null && !user.getECA().getCanContributeSpecProject()) {
+                    // set error + update response status
+                    r.addError(hash, String.format(
+                            "Project is a specification for the working group '%1$s', but user does not have permission to modify a specification project",
+                            p.getSpecWorkingGroup()), APIStatusCode.ERROR_SPEC_PROJECT);
+                    return false;
+                } else {
+                    LOGGER.debug("User '{}' was found to be a committer on current project repo '{}'", user.getMail(),
+                            p.getName());
+                    return true;
+                }
+            }
+        }
+        // check if user is a bot, either through early detection or through on-demand
+        // check
+        if ((user.getIsBot() != null && user.getIsBot()) || userIsABot(user.getMail(), filteredProjects)) {
+            LOGGER.debug("User '{} <{}>' was found to be a bot", user.getName(), user.getMail());
+            return true;
+        }
+        return false;
     }
-    Iterator<Entry<String, JsonNode>> i = bot.fields();
-    while (i.hasNext()) {
-      Entry<String, JsonNode> e = i.next();
-      // check that our field is an object with fields
-      JsonNode node = e.getValue();
-      if (node.isObject()) {
-        LOGGER.debug("Checking {} for bot email", e.getKey());
-        // if the mail matches (ignoring case) user is bot
-        JsonNode botAliasMail = node.get("email");
-        if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
-          LOGGER.debug("Found match for bot email {}", mail);
-          return true;
+
+    private boolean userIsABot(String mail, List<Project> filteredProjects) {
+        if (mail == null || "".equals(mail.trim())) {
+            return false;
+        }
+        List<JsonNode> botObjs = getBots();
+        // if there are no matching projects, then check against all bots, not just
+        // project bots
+        if (filteredProjects == null || filteredProjects.isEmpty()) {
+            return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
         }
-      }
+        // for each of the matched projects, check the bot for the matching project ID
+        for (Project p : filteredProjects) {
+            LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
+            for (JsonNode bot : botObjs) {
+                // if the project ID match, and one of the email fields matches, then user is
+                // bot
+                if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
+                        && checkFieldsForMatchingMail(bot, mail)) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
-    return false;
-  }
-
-  private boolean isAllowedUser(String mail) {
-    return allowListUsers.indexOf(mail) != -1;
-  }
-
-  /**
-   * Retrieves projects valid for the current request, or an empty list if no data
-   * or matching
-   * project repos could be found.
-   *
-   * @param req the current request
-   * @return list of matching projects for the current request, or an empty list
-   *         if none found.
-   */
-  private List<Project> retrieveProjectsForRequest(ValidationRequest req) {
-    String repoUrl = req.getRepoUrl().getPath();
-    if (repoUrl == null) {
-      LOGGER.warn("Can not match null repo URL to projects");
-      return Collections.emptyList();
+
+    /**
+     * Checks JSON node to look for email fields, both at the root, and nested email fields.
+     * 
+     * @param bot the bots JSON object representation
+     * @param mail the email to match against
+     * @return true if the bot has a matching email value, otherwise false
+     */
+    private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
+        // check the root email for the bot for match
+        JsonNode botmail = bot.get("email");
+        if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
+            LOGGER.debug("Found matching bot at root level for '{}'", mail);
+            return true;
+        }
+        Iterator<Entry<String, JsonNode>> i = bot.fields();
+        while (i.hasNext()) {
+            Entry<String, JsonNode> e = i.next();
+            // check that our field is an object with fields
+            JsonNode node = e.getValue();
+            if (node.isObject()) {
+                LOGGER.debug("Checking {} for bot email", e.getKey());
+                // if the mail matches (ignoring case) user is bot
+                JsonNode botAliasMail = node.get("email");
+                if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
+                    LOGGER.debug("Found match for bot email {}", mail);
+                    return true;
+                }
+            }
+        }
+        return false;
     }
-    // check for all projects that make use of the given repo
-    List<Project> availableProjects = projects.getProjects();
-    if (availableProjects == null || availableProjects.isEmpty()) {
-      LOGGER.warn("Could not find any projects to match against");
-      return Collections.emptyList();
+
+    private boolean isAllowedUser(String mail) {
+        return allowListUsers.indexOf(mail) != -1;
     }
-    LOGGER.debug("Checking projects for repos that end with: {}", repoUrl);
-
-    // filter the projects based on the repo URL. At least one repo in project must
-    // match the repo URL to be valid
-    if (ProviderType.GITLAB.equals(req.getProvider())) {
-      return availableProjects
-          .stream()
-          .filter(p -> p.getGitlabRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
-          .collect(Collectors.toList());
-    } else if (ProviderType.GITHUB.equals(req.getProvider())) {
-      return availableProjects
-          .stream()
-          .filter(p -> p.getGithubRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
-          .collect(Collectors.toList());
-    } else if (ProviderType.GERRIT.equals(req.getProvider())) {
-      return availableProjects
-          .stream()
-          .filter(p -> p.getGerritRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
-          .collect(Collectors.toList());
-    } else {
-      return availableProjects
-          .stream()
-          .filter(p -> p.getRepos().stream().anyMatch(re -> re.getUrl().endsWith(repoUrl)))
-          .collect(Collectors.toList());
+
+    /**
+     * Retrieves projects valid for the current request, or an empty list if no data or matching project repos could be
+     * found.
+     *
+     * @param req the current request
+     * @return list of matching projects for the current request, or an empty list if none found.
+     */
+    private List<Project> retrieveProjectsForRequest(ValidationRequest req) {
+        String repoUrl = req.getRepoUrl().getPath();
+        if (repoUrl == null) {
+            LOGGER.warn("Can not match null repo URL to projects");
+            return Collections.emptyList();
+        }
+        // check for all projects that make use of the given repo
+        List<Project> availableProjects = projects.getProjects();
+        if (availableProjects == null || availableProjects.isEmpty()) {
+            LOGGER.warn("Could not find any projects to match against");
+            return Collections.emptyList();
+        }
+        LOGGER.debug("Checking projects for repos that end with: {}", repoUrl);
+
+        // filter the projects based on the repo URL. At least one repo in project must
+        // match the repo URL to be valid
+        if (ProviderType.GITLAB.equals(req.getProvider())) {
+            return availableProjects.stream()
+                    .filter(p -> p.getGitlabRepos().stream()
+                            .anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
+                    .collect(Collectors.toList());
+        } else if (ProviderType.GITHUB.equals(req.getProvider())) {
+            return availableProjects.stream()
+                    .filter(p -> p.getGithubRepos().stream()
+                            .anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
+                    .collect(Collectors.toList());
+        } else if (ProviderType.GERRIT.equals(req.getProvider())) {
+            return availableProjects.stream()
+                    .filter(p -> p.getGerritRepos().stream()
+                            .anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
+                    .collect(Collectors.toList());
+        } else {
+            return availableProjects.stream()
+                    .filter(p -> p.getRepos().stream().anyMatch(re -> re.getUrl().endsWith(repoUrl)))
+                    .collect(Collectors.toList());
+        }
     }
-  }
-
-  /**
-   * Retrieves an Eclipse Account user object given the Git users email address
-   * (at minimum). This
-   * is facilitated using the Eclipse Foundation accounts API, along short lived
-   * in-memory caching
-   * for performance and some protection against duplicate requests.
-   *
-   * @param user the user to retrieve Eclipse Account information for
-   * @return the Eclipse Account user information if found, or null if there was
-   *         an error or no user
-   *         exists.
-   */
-  private EclipseUser getIdentifiedUser(GitUser user) {
-    // get the Eclipse account for the user
-    try {
-      // use cache to avoid asking for the same user repeatedly on repeated requests
+
+    /**
+     * Retrieves an Eclipse Account user object given the Git users email address (at minimum). This is facilitated
+     * using the Eclipse Foundation accounts API, along short lived in-memory caching for performance and some
+     * protection against duplicate requests.
+     *
+     * @param user the user to retrieve Eclipse Account information for
+     * @return the Eclipse Account user information if found, or null if there was an error or no user exists.
+     */
+    private EclipseUser getIdentifiedUser(GitUser user) {
+        // get the Eclipse account for the user
+        try {
+            // use cache to avoid asking for the same user repeatedly on repeated requests
       EclipseUser foundUser = users.getUser(user.getMail());
       if (foundUser == null) {
-        LOGGER.warn("No users found for mail '{}'", user.getMail());
-      }
+                LOGGER.warn("No users found for mail '{}'", user.getMail());
+            }
       return foundUser;
-    } catch (WebApplicationException e) {
-      Response r = e.getResponse();
-      if (r != null && r.getStatus() == 404) {
-        LOGGER.error("No users found for mail '{}'", user.getMail());
-      } else {
-        LOGGER.error("Error while checking for user", e);
-      }
+        } catch (WebApplicationException e) {
+            Response r = e.getResponse();
+            if (r != null && r.getStatus() == 404) {
+                LOGGER.error("No users found for mail '{}'", user.getMail());
+            } else {
+                LOGGER.error("Error while checking for user", e);
+            }
+        }
+        return null;
     }
-    return null;
-  }
 
-  private List<JsonNode> getBots() {
-    Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,  () -> bots.getBots());
-    if (!allBots.isPresent()) {
-      return Collections.emptyList();
+    private List<JsonNode> getBots() {
+        Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,
+                () -> bots.getBots());
+        if (!allBots.isPresent()) {
+            return Collections.emptyList();
+        }
+        return allBots.get();
     }
-    return allBots.get();
-  }
 
-  private void addMessage(ValidationResponse r, String message, String hash) {
-    addMessage(r, message, hash, APIStatusCode.SUCCESS_DEFAULT);
-  }
+    private void addMessage(ValidationResponse r, String message, String hash) {
+        addMessage(r, message, hash, APIStatusCode.SUCCESS_DEFAULT);
+    }
 
-  private void addError(ValidationResponse r, String message, String hash) {
-    addError(r, message, hash, APIStatusCode.ERROR_DEFAULT);
-  }
+    private void addError(ValidationResponse r, String message, String hash) {
+        addError(r, message, hash, APIStatusCode.ERROR_DEFAULT);
+    }
 
-  private void addMessage(ValidationResponse r, String message, String hash, APIStatusCode code) {
-    if (LOGGER.isDebugEnabled()) {
-      LOGGER.debug(message);
+    private void addMessage(ValidationResponse r, String message, String hash, APIStatusCode code) {
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug(message);
+        }
+        r.addMessage(hash, message, code);
     }
-    r.addMessage(hash, message, code);
-  }
-
-  private void addError(ValidationResponse r, String message, String hash, APIStatusCode code) {
-    LOGGER.error(message);
-    // only add as strict error for tracked projects
-    if (r.getTrackedProject() || r.getStrictMode()) {
-      r.addError(hash, message, code);
-    } else {
-      r.addWarning(hash, message, code);
+
+    private void addError(ValidationResponse r, String message, String hash, APIStatusCode code) {
+        LOGGER.error(message);
+        // only add as strict error for tracked projects
+        if (r.getTrackedProject() || r.getStrictMode()) {
+            r.addError(hash, message, code);
+        } else {
+            r.addWarning(hash, message, code);
+        }
     }
-  }
 }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 4ddc2d46..4651644d 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -7,6 +7,17 @@ eclipse.noreply.email-patterns=@users.noreply.github.com\$
 
 ## Expect to be mounted to '/git' to match current URL spec
 quarkus.http.root-path=/git
+## DATASOURCE CONFIG
+eclipse.db.default.limit=10
+eclipse.db.default.limit.max=100
+quarkus.datasource.db-kind=mariadb
+quarkus.datasource.jdbc.min-size = 5
+quarkus.datasource.jdbc.max-size = 15
+
+# Tells Quarkus which objects are associated with what databases (used to generate entity tables internally)
+quarkus.hibernate-orm.packages=org.eclipsefoundation.git.eca.dto
+quarkus.hibernate-orm.datasource=<default>
+
 quarkus.http.port=8080
 
 ## OAUTH CONFIG
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 2d81011a..907379dc 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -11,6 +11,7 @@ package org.eclipsefoundation.git.eca.resource;
 import static io.restassured.RestAssured.given;
 import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -18,6 +19,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.UUID;
 
 import javax.inject.Inject;
 
@@ -111,7 +113,7 @@ class ValidationResourceTest {
         List<Commit> commits = new ArrayList<>();
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
-                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -128,7 +130,7 @@ class ValidationResourceTest {
         // set up test users
         GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
-                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
 
         ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
@@ -143,7 +145,7 @@ class ValidationResourceTest {
         // set up test users
         GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
-                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
 
         ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
@@ -155,7 +157,6 @@ class ValidationResourceTest {
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);
         }
-        System.out.println(in);
         Assertions.assertTrue(
                 matchesJsonSchemaInClasspath(SchemaNamespaceHelper.VALIDATION_REQUEST_SCHEMA_PATH).matches(in));
         // known good request
@@ -174,7 +175,7 @@ class ValidationResourceTest {
         List<Commit> commits = new ArrayList<>();
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
-                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -201,7 +202,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things").setParents(Arrays
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things").setParents(Arrays
                         .asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10", "46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c11"))
                 .build();
         commits.add(c1);
@@ -221,7 +222,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -241,7 +242,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -263,7 +264,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), "barshallb@personal.co"))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -286,7 +287,7 @@ class ValidationResourceTest {
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Change-Id: 0000000000000001\nSigned-off-by: %s <%s>", g1.getName(),
                         g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -308,7 +309,7 @@ class ValidationResourceTest {
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>\nChange-Id: 0000000000000001", g1.getName(),
                         g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -330,7 +331,7 @@ class ValidationResourceTest {
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Change-Id: 0000000000000001\\nSigned-off-by: %s <%s>\nSigned-off-by: %s <%s>",
                         g1.getName(), g1.getMail(), g1.getName(), "barshallb@personal.co"))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -353,7 +354,7 @@ class ValidationResourceTest {
         List<Commit> commits = new ArrayList<>();
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
-                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Collections.emptyList()).build();
         commits.add(c1);
 
@@ -370,7 +371,7 @@ class ValidationResourceTest {
         // create sample commits
         c1 = Commit.builder().setAuthor(g2).setCommitter(g2)
                 .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
         vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
@@ -379,8 +380,9 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Should be invalid as Grunt does not have spec project write access
         // Should have 2 errors, as both users get validated
+
         given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
-                is(false), "errorCount", is(2), "commits.123456789abcdefghijklmnop.errors[0].code",
+                is(false), "errorCount", is(2), "commits." + c1.getHash() + ".errors[0].code",
                 is(APIStatusCode.ERROR_SPEC_PROJECT.getValue()));
     }
 
@@ -395,7 +397,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2)
                 .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -419,7 +421,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -441,7 +443,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -465,7 +467,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2)
                 .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -489,7 +491,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -513,7 +515,7 @@ class ValidationResourceTest {
         // create sample commits
         Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
                 .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
-                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
                 .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
         commits.add(c1);
 
@@ -531,7 +533,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -550,7 +552,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -569,7 +571,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -588,7 +590,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -607,7 +609,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -627,7 +629,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -647,7 +649,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -667,7 +669,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -686,7 +688,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -706,7 +708,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -726,7 +728,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -747,7 +749,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -768,7 +770,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -787,7 +789,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -807,7 +809,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -827,7 +829,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -846,7 +848,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -866,7 +868,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -886,7 +888,7 @@ class ValidationResourceTest {
 
         List<Commit> commits = new ArrayList<>();
         // create sample commits
-        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash("123456789abcdefghijklmnop")
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash(UUID.randomUUID().toString())
                 .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
                 .build();
         commits.add(c1);
@@ -898,4 +900,131 @@ class ValidationResourceTest {
         // Should be valid as grunter used a no-reply Github account and has a matching GH handle
         given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
+
+    /*
+     * 
+     * DB PERSISTENCE TESTS
+     * 
+     */
+
+    @Test
+    void validateRevalidation_success() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+
+        // repeat call to test that skipped status is passed
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0), "commits." + c1.getHash() + ".messages[0].code",
+                is(APIStatusCode.SUCCESS_SKIPPED.getValue()));
+    }
+
+    @Test
+    void validateRevalidation_errors() {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+        GitUser g2 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        // create sample commits
+        List<Commit> commits = new ArrayList<>();
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // should fail with 1 error
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+
+        // repeat call to test that previously run check still fails
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1), "commits." + c1.getHash() + ".errors[0].code",
+                is(APIStatusCode.ERROR_AUTHOR.getValue()));
+    }
+
+    @Test
+    void validateRevalidation_partialSuccess() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        GitUser g2 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        // successful commit
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+        // error commit
+        Commit c2 = Commit.builder().setAuthor(g2).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c2);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // should fail with 1 error
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+
+        // repeat call to test that previously run check still fails
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
+                is(false), "commits." + c2.getHash() + ".errors[0].code", is(APIStatusCode.ERROR_AUTHOR.getValue()),
+                "commits." + c1.getHash() + ".messages[0].code", is(APIStatusCode.SUCCESS_SKIPPED.getValue()));
+    }
+
+    @Test
+    void validateRevalidation_commitUpdatedAfterError() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        GitUser g2 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash(UUID.randomUUID().toString()).setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(Arrays.asList(c1))
+                .build();
+
+        // test output w/ assertions
+        // should fail with 1 error
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1), "commits." + c1.getHash() + ".errors[0].code",
+                is(APIStatusCode.ERROR_AUTHOR.getValue()));
+
+        // simulate fixed ECA by updating author and using same hash
+        c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail())).setHash(c1.getHash())
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(Arrays.asList(c1))
+                .build();
+
+        // repeat call to test that previously run check now passes
+        // Check that message code is not skipped
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0), "commits." + c1.getHash() + ".messages[0].code",
+                not(APIStatusCode.SUCCESS_SKIPPED.getValue()));
+    }
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/dao/TestPersistenceDao.java b/src/test/java/org/eclipsefoundation/git/eca/test/dao/TestPersistenceDao.java
new file mode 100644
index 00000000..d18cc570
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/dao/TestPersistenceDao.java
@@ -0,0 +1,27 @@
+package org.eclipsefoundation.git.eca.test.dao;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipsefoundation.persistence.dao.impl.DefaultHibernateDao;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+
+import io.quarkus.test.Mock;
+
+/**
+ * Alternate mock DAO that ignores delete operations. Hibernate does updates initially rather than delete operations in
+ * H2, which cause false negatives in tests
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Mock
+@ApplicationScoped
+public class TestPersistenceDao extends DefaultHibernateDao {
+
+    @Override
+    public <T extends BareNode> void delete(RDBMSQuery<T> q) {
+        // TODO do proper deleting code, but this will not impact basic tests/functionality
+    }
+
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 81d29deb..077ffcea 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -5,6 +5,16 @@ org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 eclipse.noreply.email-patterns=@users.noreply.github.com\$
 eclipse.mail.allowlist=noreply@github.com
 
+## DATASOURCE CONFIG
+quarkus.datasource.db-kind=h2
+eclipse.db.default.limit=25
+eclipse.db.default.limit.max=100
+quarkus.hibernate-orm.database.generation=none
+
+# Flyway configuration for the default datasource
+quarkus.flyway.locations=classpath:database/default
+quarkus.flyway.migrate-at-start=true
+
 ## Expect to be mounted to '/git' to match current URL spec
 quarkus.http.root-path=/git
 
@@ -14,5 +24,3 @@ quarkus.oidc.enabled=false
 quarkus.keycloak.devservices.enabled=false
 quarkus.oidc-client.enabled=false
 quarkus.http.port=8080
-
-#quarkus.log.level=DEBUG
\ 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
new file mode 100644
index 00000000..52a1f66d
--- /dev/null
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -0,0 +1,20 @@
+
+CREATE TABLE CommitValidationMessage (
+  sha varchar(100) DEFAULT '',
+  projectId varchar(100) DEFAULT '',
+  message varchar(255) NOT NULL,
+  CONSTRAINT message_pkey PRIMARY KEY (sha , projectId )
+);
+INSERT INTO  CommitValidationMessage(sha,projectId,message) VALUES('123456789','sample.proj','Some error message');
+
+CREATE TABLE CommitValidationStatus (
+  sha varchar(100) DEFAULT '',
+  projectId varchar(100)  DEFAULT '',
+  lastModified timestamp DEFAULT NULL,
+  creationDate timestamp DEFAULT NULL,
+  errorCount int(11) DEFAULT NULL,
+  CONSTRAINT status_pkey PRIMARY KEY (sha , projectId )
+);
+INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('123456789', 'sample.proj', NOW(), NOW(), 1);
+INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('123456789', 'sample.proto', NOW(), NOW(), 0);
+INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('987654321', 'sample.proto', NOW(), NOW(), 0);
-- 
GitLab


From 312e99619867a52254b5c2e6e6e71ea0fc4c9aa1 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 20 Apr 2022 16:02:47 -0400
Subject: [PATCH 02/10] Update Git ECA DB structure, update tests, add default
 SQL file

---
 config/mariadb/init.sql                       |  20 ++
 .../git/eca/dto/CommitValidationMessage.java  | 123 +++++++++---
 .../git/eca/dto/CommitValidationStatus.java   | 184 +++++++++---------
 .../eca/namespace/GitEcaParameterNames.java   |   6 +-
 .../git/eca/resource/ValidationResource.java  | 170 ++++++++--------
 .../database/default/V1.0.0__default.sql      |  39 ++--
 6 files changed, 328 insertions(+), 214 deletions(-)
 create mode 100644 config/mariadb/init.sql

diff --git a/config/mariadb/init.sql b/config/mariadb/init.sql
new file mode 100644
index 00000000..30d455ed
--- /dev/null
+++ b/config/mariadb/init.sql
@@ -0,0 +1,20 @@
+CREATE TABLE `CommitValidationMessage` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `commit_id` int(11) NOT NULL,
+  `providerId` varchar(100) DEFAULT NULL,
+  `authorEmail` varchar(100) DEFAULT NULL,
+  `eclipseId` varchar(255) DEFAULT NULL,
+  `statusCode` int(11) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `CommitValidationStatus` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `sha` varchar(100) NOT NULL,
+  `project` varchar(100) NOT NULL,
+  `lastModified` datetime DEFAULT NULL,
+  `creationDate` datetime DEFAULT NULL,
+  `provider` varchar(100) NOT NULL,
+  `repoUrl` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`)
+);
\ No newline at end of file
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 70c45b74..e0daa0a0 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
@@ -2,71 +2,122 @@ package org.eclipsefoundation.git.eca.dto;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import javax.persistence.EmbeddedId;
 import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
 import javax.persistence.Table;
 import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.commons.lang3.StringUtils;
 import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
-import org.eclipsefoundation.git.eca.dto.CommitValidationStatus.CommitValidationStatusCompositeId;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.persistence.dto.BareNode;
 import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
 import org.eclipsefoundation.persistence.model.DtoTable;
 import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
 import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 @Table
 @Entity
 public class CommitValidationMessage extends BareNode {
     public static final DtoTable TABLE = new DtoTable(CommitValidationMessage.class, "cvm");
 
-    @EmbeddedId
-    private CommitValidationStatusCompositeId compositeId;
-    private String message;
+    @Id
+    @GeneratedValue(strategy=GenerationType.IDENTITY)
+    private int id;
+    @ManyToOne
+    private CommitValidationStatus commit;
+    private int statusCode;
+    private String eclipseId;
+    private String authorEmail;
+    private String providerId;
 
     @Override
     public Object getId() {
-        return getCompositeId();
+        return id;
     }
 
     /**
-     * @return the compositeId
+     * @param id the id to set
      */
-    public CommitValidationStatusCompositeId getCompositeId() {
-        return compositeId;
+    public void setId(int id) {
+        this.id = id;
     }
 
     /**
-     * @param compositeId the compositeId to set
+     * @return the commit
      */
-    public void setCompositeId(CommitValidationStatusCompositeId compositeId) {
-        this.compositeId = compositeId;
+    @JsonIgnore
+    public CommitValidationStatus getCommit() {
+        return commit;
     }
 
     /**
-     * @return the message
+     * @param commit the commit to set
      */
-    public String getMessage() {
-        return message;
+    @JsonIgnore
+    public void setCommit(CommitValidationStatus commit) {
+        this.commit = commit;
     }
 
     /**
-     * @param message the message to set
+     * @return the statusCode
      */
-    public void setMessage(String message) {
-        this.message = message;
+    public int getStatusCode() {
+        return statusCode;
     }
 
-    @Override
-    public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("CommitValidationMessage [compositeId=");
-        builder.append(compositeId);
-        builder.append(", message=");
-        builder.append(message);
-        builder.append("]");
-        return builder.toString();
+    /**
+     * @param statusCode the statusCode to set
+     */
+    public void setStatusCode(int statusCode) {
+        this.statusCode = statusCode;
+    }
+
+    /**
+     * @return the providerId
+     */
+    public String getProviderId() {
+        return providerId;
+    }
+
+    /**
+     * @param providerId the providerId to set
+     */
+    public void setProviderId(String providerId) {
+        this.providerId = providerId;
+    }
+
+    /**
+     * @return the eclipseId
+     */
+    public String getEclipseId() {
+        return eclipseId;
+    }
+
+    /**
+     * @param eclipseId the eclipseId to set
+     */
+    public void setEclipseId(String eclipseId) {
+        this.eclipseId = eclipseId;
+    }
+
+    /**
+     * @return the authorEmail
+     */
+    public String getAuthorEmail() {
+        return authorEmail;
+    }
+
+    /**
+     * @param authorEmail the authorEmail to set
+     */
+    public void setAuthorEmail(String authorEmail) {
+        this.authorEmail = authorEmail;
     }
 
     @Singleton
@@ -77,11 +128,19 @@ public class CommitValidationMessage extends BareNode {
         @Override
         public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
             ParameterizedSQLStatement stmt = builder.build(TABLE);
-            // sha check
-            String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
-            if (StringUtils.isNumeric(id)) {
-                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".compositeId.sha = ?",
-                        new Object[] { id }));
+            if (isRoot) {
+                // id check
+                String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
+                if (StringUtils.isNumeric(id)) {
+                    stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?",
+                            new Object[] { Integer.valueOf(id) }));
+                }
+                // commit id check
+                String commitId = params.getFirst(GitEcaParameterNames.COMMIT_ID.getName());
+                if (StringUtils.isNumeric(commitId)) {
+                    stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitId = ?",
+                            new Object[] { Integer.valueOf(commitId) }));
+                }
             }
             return stmt;
         }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
index ae2554d3..7cc003c6 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
@@ -1,22 +1,23 @@
 package org.eclipsefoundation.git.eca.dto;
 
-import java.io.Serializable;
 import java.time.ZonedDateTime;
 import java.util.List;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import javax.persistence.Embeddable;
-import javax.persistence.EmbeddedId;
 import javax.persistence.Entity;
-import javax.persistence.JoinColumn;
-import javax.persistence.JoinColumns;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
 import javax.persistence.OneToMany;
 import javax.persistence.Table;
 import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.commons.lang3.StringUtils;
-import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.eclipsefoundation.git.eca.namespace.ProviderType;
 import org.eclipsefoundation.persistence.dto.BareNode;
 import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
 import org.eclipsefoundation.persistence.model.DtoTable;
@@ -28,47 +29,82 @@ import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 public class CommitValidationStatus extends BareNode {
     public static final DtoTable TABLE = new DtoTable(CommitValidationStatus.class, "cvs");
 
-    @EmbeddedId
-    private CommitValidationStatusCompositeId compositeId;
-    private int errorCount;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String sha;
+    private String project;
+    private String repoUrl;
+    @Enumerated(EnumType.STRING)
+    private ProviderType provider;
     private ZonedDateTime creationDate;
     private ZonedDateTime lastModified;
-    @OneToMany
-    @JoinColumns({ @JoinColumn(name = "sha", referencedColumnName = "sha", updatable = false),
-            @JoinColumn(name = "projectId", referencedColumnName = "projectId", updatable = false) })
-    private List<CommitValidationMessage> messages;
+    @OneToMany(mappedBy = "commit")
+    private List<CommitValidationMessage> errors;
 
     @Override
     public Object getId() {
-        return getCompositeId();
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the sha
+     */
+    public String getSha() {
+        return sha;
+    }
+
+    /**
+     * @param sha the sha to set
+     */
+    public void setSha(String sha) {
+        this.sha = sha;
+    }
+
+    /**
+     * @return the project
+     */
+    public String getProject() {
+        return project;
+    }
+
+    /**
+     * @param project the project to set
+     */
+    public void setProject(String project) {
+        this.project = project;
     }
 
     /**
-     * @return the compositeId
+     * @return the repoUrl
      */
-    public CommitValidationStatusCompositeId getCompositeId() {
-        return compositeId;
+    public String getRepoUrl() {
+        return repoUrl;
     }
 
     /**
-     * @param compositeId the compositeId to set
+     * @param repoUrl the repoUrl to set
      */
-    public void setCompositeId(CommitValidationStatusCompositeId compositeId) {
-        this.compositeId = compositeId;
+    public void setRepoUrl(String repoUrl) {
+        this.repoUrl = repoUrl;
     }
 
     /**
-     * @return the errorCount
+     * @return the provider
      */
-    public int getErrorCount() {
-        return errorCount;
+    public ProviderType getProvider() {
+        return provider;
     }
 
     /**
-     * @param errorCount the errorCount to set
+     * @param provider the provider to set
      */
-    public void setErrorCount(int errorCount) {
-        this.errorCount = errorCount;
+    public void setProvider(ProviderType provider) {
+        this.provider = provider;
     }
 
     /**
@@ -102,81 +138,40 @@ public class CommitValidationStatus extends BareNode {
     /**
      * @return the messages
      */
-    public List<CommitValidationMessage> getMessages() {
-        return messages;
+    public List<CommitValidationMessage> getErrors() {
+        return errors;
     }
 
     /**
-     * @param messages the messages to set
+     * @param errors the errors to set
      */
-    public void setMessages(List<CommitValidationMessage> messages) {
-        this.messages = messages;
+    public void setErrors(List<CommitValidationMessage> errors) {
+        this.errors = errors;
     }
 
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
-        builder.append("CommitValidationStatus [compositeId=");
-        builder.append(compositeId);
-        builder.append(", errorCount=");
-        builder.append(errorCount);
+        builder.append("CommitValidationStatus [id=");
+        builder.append(id);
+        builder.append(", sha=");
+        builder.append(sha);
+        builder.append(", project=");
+        builder.append(project);
+        builder.append(", repoUrl=");
+        builder.append(repoUrl);
+        builder.append(", provider=");
+        builder.append(provider);
         builder.append(", creationDate=");
         builder.append(creationDate);
         builder.append(", lastModified=");
         builder.append(lastModified);
         builder.append(", messages=");
-        builder.append(messages);
+        builder.append(errors);
         builder.append("]");
         return builder.toString();
     }
 
-    @Embeddable
-    public static class CommitValidationStatusCompositeId implements Serializable {
-        private static final long serialVersionUID = 1L;
-        private String sha;
-        private String projectId;
-
-        /**
-         * @return the sha
-         */
-        public String getSha() {
-            return sha;
-        }
-
-        /**
-         * @param sha the sha to set
-         */
-        public void setSha(String sha) {
-            this.sha = sha;
-        }
-
-        /**
-         * @return the projectId
-         */
-        public String getProjectId() {
-            return projectId;
-        }
-
-        /**
-         * @param projectId the projectId to set
-         */
-        public void setProjectId(String projectId) {
-            this.projectId = projectId;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder builder = new StringBuilder();
-            builder.append("CommitValidationStatusCompositeId [sha=");
-            builder.append(sha);
-            builder.append(", projectId=");
-            builder.append(projectId);
-            builder.append("]");
-            return builder.toString();
-        }
-
-    }
-
     @Singleton
     public static class CommitValidationStatusFilter implements DtoFilter<CommitValidationStatus> {
         @Inject
@@ -186,10 +181,25 @@ public class CommitValidationStatus extends BareNode {
         public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
             ParameterizedSQLStatement stmt = builder.build(TABLE);
             // sha check
-            String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
-            if (StringUtils.isNumeric(id)) {
-                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".compositeId.sha = ?",
-                        new Object[] { id }));
+            String sha = params.getFirst(GitEcaParameterNames.SHA.getName());
+            if (StringUtils.isNumeric(sha)) {
+                stmt.addClause(
+                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".sha = ?", new Object[] { sha }));
+            }
+            String projectId = params.getFirst(GitEcaParameterNames.PROJECT_ID.getName());
+            if (StringUtils.isNumeric(projectId)) {
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?",
+                        new Object[] { projectId }));
+            }
+            List<String> shas = params.get(GitEcaParameterNames.SHAS.getName());
+            if (shas != null && !shas.isEmpty()) {
+                stmt.addClause(
+                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".sha IN ?", new Object[] { shas }));
+            }
+            String repoUrl = params.getFirst(GitEcaParameterNames.REPO_URL.getName());
+            if (StringUtils.isNotBlank(repoUrl)) {
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repoUrl = ?",
+                        new Object[] { repoUrl }));
             }
             return stmt;
         }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
index d998635d..880aea77 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -9,16 +9,20 @@ import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
 
 @Singleton
 public final class GitEcaParameterNames implements UrlParameterNamespace {
+    public static final String COMMIT_ID_RAW = "commit_id";
     public static final String SHA_RAW = "sha";
     public static final String SHAS_RAW = "shas";
     public static final String PROJECT_ID_RAW = "project_id";
+    public static final String REPO_URL_RAW = "repo_url";
+    public static final UrlParameter COMMIT_ID = new UrlParameter(COMMIT_ID_RAW);
     public static final UrlParameter SHA = new UrlParameter(SHA_RAW);
     public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW);
     public static final UrlParameter PROJECT_ID = new UrlParameter(PROJECT_ID_RAW);
+    public static final UrlParameter REPO_URL = new UrlParameter(REPO_URL_RAW);
 
     @Override
     public List<UrlParameter> getParameters() {
-        return Arrays.asList(SHA, SHAS, PROJECT_ID);
+        return Arrays.asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, REPO_URL);
     }
 
 }
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 b0023d18..c82b9e04 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -19,9 +19,11 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
@@ -35,7 +37,6 @@ import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
-import org.eclipsefoundation.git.eca.dto.CommitValidationStatus.CommitValidationStatusCompositeId;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.CommitStatus;
@@ -122,15 +123,14 @@ public class ValidationResource {
             ValidationResponse r = ValidationResponse.builder()
                     .setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
                     .setTrackedProject(!filteredProjects.isEmpty()).build();
-            String projectIdentifier = filteredProjects.isEmpty() ? req.getRepoUrl().toString()
-                    : filteredProjects.get(0).getProjectId();
-            List<CommitValidationStatus> statuses = getCurrentCommitValidationStatus(req, projectIdentifier);
+            List<CommitValidationStatus> statuses = getCurrentCommitValidationStatus(req,
+                    filteredProjects.isEmpty() ? null : filteredProjects.get(0).getProjectId());
             for (Commit c : req.getCommits()) {
                 // get the current status if present
-                Optional<CommitValidationStatus> status = statuses.stream()
-                        .filter(s -> c.getHash().equals(s.getCompositeId().getSha())).findFirst();
+                Optional<CommitValidationStatus> status = statuses.stream().filter(s -> c.getHash().equals(s.getSha()))
+                        .findFirst();
                 // skip the commit validation if already passed
-                if (status.isPresent() && status.get().getErrorCount() == 0) {
+                if (status.isPresent() && status.get().getErrors().isEmpty()) {
                     r.addMessage(c.getHash(), "Commit was previously validated, skipping processing",
                             APIStatusCode.SUCCESS_SKIPPED);
                     continue;
@@ -143,7 +143,7 @@ public class ValidationResource {
                     break;
                 }
             }
-            updateCommitValidationStatus(r, projectIdentifier, statuses);
+            updateCommitValidationStatus(r, req, statuses, filteredProjects.isEmpty() ? null : filteredProjects.get(0));
             return r.toResponse();
         } else {
             // create a stubbed response with the errors
@@ -153,78 +153,15 @@ public class ValidationResource {
         }
     }
 
-    private void updateCommitValidationStatus(ValidationResponse r, String projectIdentifier,
-            List<CommitValidationStatus> statuses) {
-        // remove existing messaging
-        RDBMSQuery<CommitValidationMessage> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class),
-                getCommitParams(r, projectIdentifier));
-        // if present, remove any current validation messages as we will be pushing new ones
-        dao.delete(q);
-        List<CommitValidationMessage> messages = new ArrayList<>();
-        List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
-        for (Entry<String, CommitStatus> e : r.getCommits().entrySet()) {
-            if (ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())) {
-                LOGGER.warn("Not logging errors/validation associated with unknown commit");
-                continue;
-            }
-            // if there are errors, update validation messages
-            if (e.getValue().getErrors().size() > 0) {
-                messages.addAll(e.getValue().getErrors().stream().map(err -> {
-                    CommitValidationMessage m = new CommitValidationMessage();
-                    CommitValidationStatusCompositeId id = new CommitValidationStatusCompositeId();
-                    id.setProjectId(projectIdentifier);
-                    id.setSha(e.getKey());
-                    m.setCompositeId(id);
-                    m.setMessage(err.getMessage());
-                    return m;
-                }).collect(Collectors.toList()));
-            }
-            // update the status if present, otherwise make new one.
-            Optional<CommitValidationStatus> status = statuses.stream()
-                    .filter(s -> e.getKey().equals(s.getCompositeId().getSha())).findFirst();
-            CommitValidationStatus base;
-            if (status.isPresent()) {
-                base = status.get();
-            } else {
-                base = new CommitValidationStatus();
-                CommitValidationStatusCompositeId id = new CommitValidationStatusCompositeId();
-                id.setProjectId(projectIdentifier);
-                id.setSha(e.getKey());
-                base.setCompositeId(id);
-                base.setCreationDate(DateTimeHelper.now());
-            }
-            base.setErrorCount(e.getValue().getErrors().size());
-            base.setLastModified(DateTimeHelper.now());
-            updatedStatuses.add(base);
+    @GET
+    @Path("status")
+    public Response getCommitValidation(@QueryParam(GitEcaParameterNames.SHAS_RAW) List<String> shas) {
+        if (shas.isEmpty()) {
+            return Response.noContent().build();
         }
-        // update the base commit status and messages
-        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
-        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
-    }
-
-    private List<CommitValidationStatus> getCurrentCommitValidationStatus(ValidationRequest req,
-            String projectIdentifier) {
         RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
-                getCommitParams(req, projectIdentifier));
-        // set use limit to false to collect all data in one request
-        q.setUseLimit(false);
-        return dao.get(q);
-    }
-
-    private MultivaluedMap<String, String> getCommitParams(ValidationRequest req, String projectIdentifier) {
-        return getCommitParams(req.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()),
-                projectIdentifier);
-    }
-
-    private MultivaluedMap<String, String> getCommitParams(ValidationResponse r, String projectIdentifier) {
-        return getCommitParams(List.copyOf(r.getCommits().keySet()), projectIdentifier);
-    }
-
-    private MultivaluedMap<String, String> getCommitParams(List<String> commitShas, String projectIdentifier) {
-        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
-        params.put(GitEcaParameterNames.SHAS_RAW, commitShas);
-        params.add(GitEcaParameterNames.PROJECT_ID_RAW, projectIdentifier);
-        return params;
+                getCommitParams(shas, null, null));
+        return Response.ok(dao.get(q)).build();
     }
 
     /**
@@ -325,6 +262,83 @@ public class ValidationResource {
         return true;
     }
 
+    private void updateCommitValidationStatus(ValidationResponse r, ValidationRequest req,
+            List<CommitValidationStatus> statuses, Project p) {
+        // remove existing messaging
+        RDBMSQuery<CommitValidationMessage> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class),
+                getCommitParams(req, p != null ? p.getProjectId() : null));
+
+        List<CommitValidationMessage> messages = new ArrayList<>();
+        List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
+        for (Entry<String, CommitStatus> e : r.getCommits().entrySet()) {
+            if (ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())) {
+                LOGGER.warn("Not logging errors/validation associated with unknown commit");
+                continue;
+            }
+            // update the status if present, otherwise make new one.
+            Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getSha()))
+                    .findFirst();
+            CommitValidationStatus base;
+            if (status.isPresent()) {
+                base = status.get();
+            } else {
+                base = new CommitValidationStatus();
+                base.setProject(p != null ? p.getProjectId() : null);
+                base.setSha(e.getKey());
+                base.setProvider(req.getProvider());
+                base.setRepoUrl(req.getRepoUrl().toString());
+                base.setCreationDate(DateTimeHelper.now());
+            }
+            base.setLastModified(DateTimeHelper.now());
+            updatedStatuses.add(base);
+            // get the commit for current status
+            Optional<Commit> commit = req.getCommits().stream().filter(c -> c.getHash().equals(e.getKey())).findFirst();
+            if (commit.isEmpty()) {
+                // TODO
+            }
+            Commit c = commit.get();
+            // if there are errors, update validation messages
+            if (e.getValue().getErrors().size() > 0) {
+                messages.addAll(e.getValue().getErrors().stream().map(err -> {
+                    CommitValidationMessage m = new CommitValidationMessage();
+                    m.setAuthorEmail(c.getAuthor().getMail());
+                    m.setStatusCode(err.getCode().getValue());
+                    // TODO add a checked way to set this
+                    m.setEclipseId(null);
+                    m.setProviderId(null);
+                    m.setCommit(base);
+                    return m;
+                }).collect(Collectors.toList()));
+            }
+        }
+        // update the base commit status and messages
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
+        // if present, remove any current validation messages as we will be pushing new ones
+        dao.delete(q);
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
+    }
+
+    private List<CommitValidationStatus> getCurrentCommitValidationStatus(ValidationRequest req, String projectId) {
+        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
+                getCommitParams(req, projectId));
+        // set use limit to false to collect all data in one request
+        q.setUseLimit(false);
+        return dao.get(q);
+    }
+
+    private MultivaluedMap<String, String> getCommitParams(ValidationRequest req, String projectId) {
+        return getCommitParams(req.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()), projectId,
+                req.getRepoUrl().toString());
+    }
+
+    private MultivaluedMap<String, String> getCommitParams(List<String> commitShas, String projectId, String repoUrl) {
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.put(GitEcaParameterNames.SHAS_RAW, commitShas);
+        params.add(GitEcaParameterNames.PROJECT_ID_RAW, projectId);
+        params.add(GitEcaParameterNames.REPO_URL_RAW, repoUrl);
+        return params;
+    }
+
     /**
      * Validates author access for the current commit. If there are errors, they are recorded in the response for the
      * current request to be returned once all validation checks are completed.
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 52a1f66d..1614a2e5 100644
--- a/src/test/resources/database/default/V1.0.0__default.sql
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -1,20 +1,27 @@
 
-CREATE TABLE CommitValidationMessage (
-  sha varchar(100) DEFAULT '',
-  projectId varchar(100) DEFAULT '',
-  message varchar(255) NOT NULL,
-  CONSTRAINT message_pkey PRIMARY KEY (sha , projectId )
-);
-INSERT INTO  CommitValidationMessage(sha,projectId,message) VALUES('123456789','sample.proj','Some error message');
 
 CREATE TABLE CommitValidationStatus (
-  sha varchar(100) DEFAULT '',
-  projectId varchar(100)  DEFAULT '',
-  lastModified timestamp DEFAULT NULL,
-  creationDate timestamp DEFAULT NULL,
-  errorCount int(11) DEFAULT NULL,
-  CONSTRAINT status_pkey PRIMARY KEY (sha , projectId )
+  sha varchar(100) NOT NULL,
+  project varchar(100) DEFAULT NULL,
+  lastModified datetime DEFAULT NULL,
+  creationDate datetime DEFAULT NULL,
+  id int(11) NOT NULL AUTO_INCREMENT,
+  provider varchar(100) NOT NULL,
+  repoUrl varchar(100) NOT NULL,
+  PRIMARY KEY (id)
+);
+INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(1,'123456789', 'sample.proj', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(2,'123456789', 'sample.proto', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(3,'987654321', 'sample.proto', NOW(), NOW(),'GITLAB','');
+
+CREATE TABLE CommitValidationMessage (
+  providerId varchar(100) DEFAULT NULL,
+  authorEmail varchar(100) DEFAULT NULL,
+  eclipseId varchar(255) DEFAULT NULL,
+  id int(11) NOT NULL AUTO_INCREMENT,
+  commit_id int(11) NOT NULL,
+  statusCode int(11) NOT NULL,
+  PRIMARY KEY (id)
 );
-INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('123456789', 'sample.proj', NOW(), NOW(), 1);
-INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('123456789', 'sample.proto', NOW(), NOW(), 0);
-INSERT INTO CommitValidationStatus(sha,projectId,lastModified,creationDate,errorCount) VALUES('987654321', 'sample.proto', NOW(), NOW(), 0);
+
+INSERT INTO  CommitValidationMessage(id,commit_id,providerId,authorEmail,eclipseId,statusCode) VALUES(4,1,'','','',-405);
-- 
GitLab


From 03b8b5593ed680091a3e2d8347ec1f839ea039d0 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 18 May 2022 14:50:42 -0400
Subject: [PATCH 03/10] Move logic from validation resource to user and new
 validation services

This clean up should make the validation resource easier to maintain and
read for future maintainers.
---
 .../git/eca/helper/CommitHelper.java          |  81 ++++---
 .../git/eca/resource/ValidationResource.java  | 212 +++---------------
 .../git/eca/service/UserService.java          |  28 +++
 .../git/eca/service/ValidationService.java    |  52 +++++
 .../eca/service/impl/CachedUserService.java   |  80 ++++++-
 .../impl/DefaultValidationService.java        | 110 +++++++++
 6 files changed, 345 insertions(+), 218 deletions(-)
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java

diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
index 2fcb738f..6bd207e7 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
@@ -9,7 +9,15 @@
  ******************************************************************************/
 package org.eclipsefoundation.git.eca.helper;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.MultivaluedMap;
+
 import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 
 /**
  * Contains helpers for processing commits.
@@ -19,34 +27,47 @@ import org.eclipsefoundation.git.eca.model.Commit;
  */
 public class CommitHelper {
 
-	/**
-	 * Validate the commits fields.
-	 *
-	 * @param c commit to validate
-	 * @return true if valid, otherwise false
-	 */
-	public static boolean validateCommit(Commit c) {
-		if (c == null) {
-			return false;
-		}
-
-		boolean valid = true;
-		// check current commit data
-		if (c.getHash() == null) {
-			valid = false;
-		}
-		// check author
-		if (c.getAuthor() == null || c.getAuthor().getMail() == null) {
-			valid = false;
-		}
-		// check committer
-		if (c.getCommitter() == null || c.getCommitter().getMail() == null) {
-			valid = false;
-		}
-
-		return valid;
-	}
-
-	private CommitHelper() {
-	}
+    /**
+     * Validate the commits fields.
+     *
+     * @param c commit to validate
+     * @return true if valid, otherwise false
+     */
+    public static boolean validateCommit(Commit c) {
+        if (c == null) {
+            return false;
+        }
+
+        boolean valid = true;
+        // check current commit data
+        if (c.getHash() == null) {
+            valid = false;
+        }
+        // check author
+        if (c.getAuthor() == null || c.getAuthor().getMail() == null) {
+            valid = false;
+        }
+        // check committer
+        if (c.getCommitter() == null || c.getCommitter().getMail() == null) {
+            valid = false;
+        }
+
+        return valid;
+    }
+
+    public static MultivaluedMap<String, String> getCommitParams(ValidationRequest req, String projectId) {
+        return getCommitParams(req.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()), projectId,
+                req.getRepoUrl().toString());
+    }
+
+    public static MultivaluedMap<String, String> getCommitParams(List<String> commitShas, String projectId, String repoUrl) {
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.put(GitEcaParameterNames.SHAS_RAW, commitShas);
+        params.add(GitEcaParameterNames.PROJECT_ID_RAW, projectId);
+        params.add(GitEcaParameterNames.REPO_URL_RAW, repoUrl);
+        return params;
+    }
+
+    private CommitHelper() {
+    }
 }
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 c82b9e04..f445a133 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -11,9 +11,7 @@ package org.eclipsefoundation.git.eca.resource;
 import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
@@ -22,43 +20,31 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipse.microprofile.rest.client.inject.RestClient;
-import org.eclipsefoundation.core.helper.DateTimeHelper;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.CachingService;
-import org.eclipsefoundation.git.eca.api.BotsAPI;
-import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
-import org.eclipsefoundation.git.eca.model.CommitStatus;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
-import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
 import org.eclipsefoundation.git.eca.service.ProjectsService;
 import org.eclipsefoundation.git.eca.service.UserService;
-import org.eclipsefoundation.persistence.dao.PersistenceDao;
-import org.eclipsefoundation.persistence.model.RDBMSQuery;
-import org.eclipsefoundation.persistence.service.FilterService;
-import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.eclipsefoundation.git.eca.service.ValidationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.fasterxml.jackson.databind.JsonNode;
-
 /**
  * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate
  * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request
@@ -82,23 +68,15 @@ public class ValidationResource {
     @Inject
     RequestWrapper wrapper;
 
-    @Inject
-    PersistenceDao dao;
-    @Inject
-    FilterService filters;
-
-    // eclipse API rest client interfaces
-    @Inject
-    UserService users;
-    @Inject
-    @RestClient
-    BotsAPI bots;
-
     // external API/service harnesses
     @Inject
     CachingService cache;
     @Inject
     ProjectsService projects;
+    @Inject
+    UserService users;
+    @Inject
+    ValidationService validation;
 
     /**
      * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository
@@ -123,7 +101,7 @@ public class ValidationResource {
             ValidationResponse r = ValidationResponse.builder()
                     .setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
                     .setTrackedProject(!filteredProjects.isEmpty()).build();
-            List<CommitValidationStatus> statuses = getCurrentCommitValidationStatus(req,
+            List<CommitValidationStatus> statuses = validation.getRequestCommitValidationStatus(wrapper, req,
                     filteredProjects.isEmpty() ? null : filteredProjects.get(0).getProjectId());
             for (Commit c : req.getCommits()) {
                 // get the current status if present
@@ -143,7 +121,8 @@ public class ValidationResource {
                     break;
                 }
             }
-            updateCommitValidationStatus(r, req, statuses, filteredProjects.isEmpty() ? null : filteredProjects.get(0));
+            validation.updateCommitValidationStatus(wrapper, r, req, statuses,
+                    filteredProjects.isEmpty() ? null : filteredProjects.get(0));
             return r.toResponse();
         } else {
             // create a stubbed response with the errors
@@ -154,14 +133,16 @@ public class ValidationResource {
     }
 
     @GET
-    @Path("status")
-    public Response getCommitValidation(@QueryParam(GitEcaParameterNames.SHAS_RAW) List<String> shas) {
-        if (shas.isEmpty()) {
-            return Response.noContent().build();
-        }
-        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
-                getCommitParams(shas, null, null));
-        return Response.ok(dao.get(q)).build();
+    @Path("status/{fingerprint}")
+    public Response getCommitValidation(@PathParam("fingerprint") String fingerprint) {
+        return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build();
+    }
+
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    @Path("status/{fingerprint}/ui")
+    public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
+        return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build();
     }
 
     /**
@@ -223,7 +204,7 @@ public class ValidationResource {
         // retrieve the eclipse account for the author
         EclipseUser eclipseAuthor = getIdentifiedUser(author);
         // if the user is a bot, generate a stubbed user
-        if (isAllowedUser(author.getMail()) || userIsABot(author.getMail(), filteredProjects)) {
+        if (isAllowedUser(author.getMail()) || users.userIsABot(author.getMail(), filteredProjects)) {
             addMessage(response, String.format("Automated user '%1$s' detected for author of commit %2$s",
                     author.getMail(), c.getHash()), c.getHash());
             eclipseAuthor = EclipseUser.createBotStub(author);
@@ -239,7 +220,7 @@ public class ValidationResource {
         // retrieve the eclipse account for the committer
         EclipseUser eclipseCommitter = getIdentifiedUser(committer);
         // check if whitelisted or bot
-        if (isAllowedUser(committer.getMail()) || userIsABot(committer.getMail(), filteredProjects)) {
+        if (isAllowedUser(committer.getMail()) || users.userIsABot(committer.getMail(), filteredProjects)) {
             addMessage(response, String.format("Automated user '%1$s' detected for committer of commit %2$s",
                     committer.getMail(), c.getHash()), c.getHash());
             eclipseCommitter = EclipseUser.createBotStub(committer);
@@ -262,83 +243,6 @@ public class ValidationResource {
         return true;
     }
 
-    private void updateCommitValidationStatus(ValidationResponse r, ValidationRequest req,
-            List<CommitValidationStatus> statuses, Project p) {
-        // remove existing messaging
-        RDBMSQuery<CommitValidationMessage> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class),
-                getCommitParams(req, p != null ? p.getProjectId() : null));
-
-        List<CommitValidationMessage> messages = new ArrayList<>();
-        List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
-        for (Entry<String, CommitStatus> e : r.getCommits().entrySet()) {
-            if (ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())) {
-                LOGGER.warn("Not logging errors/validation associated with unknown commit");
-                continue;
-            }
-            // update the status if present, otherwise make new one.
-            Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getSha()))
-                    .findFirst();
-            CommitValidationStatus base;
-            if (status.isPresent()) {
-                base = status.get();
-            } else {
-                base = new CommitValidationStatus();
-                base.setProject(p != null ? p.getProjectId() : null);
-                base.setSha(e.getKey());
-                base.setProvider(req.getProvider());
-                base.setRepoUrl(req.getRepoUrl().toString());
-                base.setCreationDate(DateTimeHelper.now());
-            }
-            base.setLastModified(DateTimeHelper.now());
-            updatedStatuses.add(base);
-            // get the commit for current status
-            Optional<Commit> commit = req.getCommits().stream().filter(c -> c.getHash().equals(e.getKey())).findFirst();
-            if (commit.isEmpty()) {
-                // TODO
-            }
-            Commit c = commit.get();
-            // if there are errors, update validation messages
-            if (e.getValue().getErrors().size() > 0) {
-                messages.addAll(e.getValue().getErrors().stream().map(err -> {
-                    CommitValidationMessage m = new CommitValidationMessage();
-                    m.setAuthorEmail(c.getAuthor().getMail());
-                    m.setStatusCode(err.getCode().getValue());
-                    // TODO add a checked way to set this
-                    m.setEclipseId(null);
-                    m.setProviderId(null);
-                    m.setCommit(base);
-                    return m;
-                }).collect(Collectors.toList()));
-            }
-        }
-        // update the base commit status and messages
-        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
-        // if present, remove any current validation messages as we will be pushing new ones
-        dao.delete(q);
-        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
-    }
-
-    private List<CommitValidationStatus> getCurrentCommitValidationStatus(ValidationRequest req, String projectId) {
-        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
-                getCommitParams(req, projectId));
-        // set use limit to false to collect all data in one request
-        q.setUseLimit(false);
-        return dao.get(q);
-    }
-
-    private MultivaluedMap<String, String> getCommitParams(ValidationRequest req, String projectId) {
-        return getCommitParams(req.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()), projectId,
-                req.getRepoUrl().toString());
-    }
-
-    private MultivaluedMap<String, String> getCommitParams(List<String> commitShas, String projectId, String repoUrl) {
-        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
-        params.put(GitEcaParameterNames.SHAS_RAW, commitShas);
-        params.add(GitEcaParameterNames.PROJECT_ID_RAW, projectId);
-        params.add(GitEcaParameterNames.REPO_URL_RAW, repoUrl);
-        return params;
-    }
-
     /**
      * Validates author access for the current commit. If there are errors, they are recorded in the response for the
      * current request to be returned once all validation checks are completed.
@@ -433,70 +337,13 @@ public class ValidationResource {
         }
         // check if user is a bot, either through early detection or through on-demand
         // check
-        if ((user.getIsBot() != null && user.getIsBot()) || userIsABot(user.getMail(), filteredProjects)) {
+        if ((user.getIsBot() != null && user.getIsBot()) || users.userIsABot(user.getMail(), filteredProjects)) {
             LOGGER.debug("User '{} <{}>' was found to be a bot", user.getName(), user.getMail());
             return true;
         }
         return false;
     }
 
-    private boolean userIsABot(String mail, List<Project> filteredProjects) {
-        if (mail == null || "".equals(mail.trim())) {
-            return false;
-        }
-        List<JsonNode> botObjs = getBots();
-        // if there are no matching projects, then check against all bots, not just
-        // project bots
-        if (filteredProjects == null || filteredProjects.isEmpty()) {
-            return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
-        }
-        // for each of the matched projects, check the bot for the matching project ID
-        for (Project p : filteredProjects) {
-            LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
-            for (JsonNode bot : botObjs) {
-                // if the project ID match, and one of the email fields matches, then user is
-                // bot
-                if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
-                        && checkFieldsForMatchingMail(bot, mail)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Checks JSON node to look for email fields, both at the root, and nested email fields.
-     * 
-     * @param bot the bots JSON object representation
-     * @param mail the email to match against
-     * @return true if the bot has a matching email value, otherwise false
-     */
-    private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
-        // check the root email for the bot for match
-        JsonNode botmail = bot.get("email");
-        if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
-            LOGGER.debug("Found matching bot at root level for '{}'", mail);
-            return true;
-        }
-        Iterator<Entry<String, JsonNode>> i = bot.fields();
-        while (i.hasNext()) {
-            Entry<String, JsonNode> e = i.next();
-            // check that our field is an object with fields
-            JsonNode node = e.getValue();
-            if (node.isObject()) {
-                LOGGER.debug("Checking {} for bot email", e.getKey());
-                // if the mail matches (ignoring case) user is bot
-                JsonNode botAliasMail = node.get("email");
-                if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
-                    LOGGER.debug("Found match for bot email {}", mail);
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
     private boolean isAllowedUser(String mail) {
         return allowListUsers.indexOf(mail) != -1;
     }
@@ -558,11 +405,11 @@ public class ValidationResource {
         // get the Eclipse account for the user
         try {
             // use cache to avoid asking for the same user repeatedly on repeated requests
-      EclipseUser foundUser = users.getUser(user.getMail());
-      if (foundUser == null) {
+            EclipseUser foundUser = users.getUser(user.getMail());
+            if (foundUser == null) {
                 LOGGER.warn("No users found for mail '{}'", user.getMail());
             }
-      return foundUser;
+            return foundUser;
         } catch (WebApplicationException e) {
             Response r = e.getResponse();
             if (r != null && r.getStatus() == 404) {
@@ -574,15 +421,6 @@ public class ValidationResource {
         return null;
     }
 
-    private List<JsonNode> getBots() {
-        Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,
-                () -> bots.getBots());
-        if (!allBots.isPresent()) {
-            return Collections.emptyList();
-        }
-        return allBots.get();
-    }
-
     private void addMessage(ValidationResponse r, String message, String hash) {
         addMessage(r, message, hash, APIStatusCode.SUCCESS_DEFAULT);
     }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
index 377b9a0d..9553a3eb 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
@@ -1,10 +1,38 @@
 package org.eclipsefoundation.git.eca.service;
 
+import java.util.List;
+
 import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.model.Project;
 
 public interface UserService {
 
+    /**
+     * Retrieves an Eclipse Account user object given the Git users email address (at minimum). This is facilitated
+     * using the Eclipse Foundation accounts API, along short lived in-memory caching for performance and some
+     * protection against duplicate requests.
+     *
+     * @param mail the email address to use to retrieve the Eclipse account for.
+     * @return the Eclipse Account user information if found, or null if there was an error or no user exists.
+     */
     EclipseUser getUser(String mail);
 
+    /**
+     * Retrieves an Eclipse Account user object given the Github username. This is facilitated using the Eclipse
+     * Foundation accounts API, along short lived in-memory caching for performance and some protection against
+     * duplicate requests.
+     *
+     * @param username the Github username used for retrieval of associated Eclipse Account if it exists.
+     * @return the Eclipse Account user information if found, or null if there was an error or no user exists.
+     */
     EclipseUser getUserByGithubUsername(String username);
+
+    /**
+     * Checks the bot API to see whether passed email address is registered to a bot under the passed projects.
+     * 
+     * @param mail the potential bot user's email address
+     * @param filteredProjects the projects to check for bot presence.
+     * @return true if the user is a bot on at least one of the given projects, false otherwise.
+     */
+    boolean userIsABot(String mail, List<Project> filteredProjects);
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
new file mode 100644
index 00000000..0c7779a8
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
@@ -0,0 +1,52 @@
+package org.eclipsefoundation.git.eca.service;
+
+import java.util.List;
+
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.model.Project;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.model.ValidationResponse;
+
+/**
+ * 
+ * @author martin
+ *
+ */
+public interface ValidationService {
+
+    /**
+     * Retrieves a set of validation status objects given the validation request fingerprint.
+     * 
+     * @param wrapper current request wrapper object
+     * @param fingerprint the validation request fingerprint
+     * @return the list of historic validation status objects, or an empty list.
+     */
+    public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint);
+
+    /**
+     * Retrieves a set of commit validation status objects given a validation request and target project.
+     * 
+     * @param wrapper current request wrapper object
+     * @param req the current validation request
+     * @param projectId the project targeted by the validation request
+     * @return the list of existing validation status objects for the validation request, or an empty list.
+     */
+    public List<CommitValidationStatus> getRequestCommitValidationStatus(RequestWrapper wrapper, ValidationRequest req,
+            String projectId);
+
+    /**
+     * Updates or creates validation status objects for the commits validated as part of the current validation request.
+     * Uses information from both the original request and the final response to generate details to be preserved in
+     * commit status objects.
+     * 
+     * @param wrapper current request wrapper object
+     * @param r the final validation response
+     * @param req the current validation request
+     * @param statuses list of existing commit validation objects to update
+     * @param p the project targeted by the validation request.
+     */
+    public void updateCommitValidationStatus(RequestWrapper wrapper, ValidationResponse r, ValidationRequest req,
+            List<CommitValidationStatus> statuses, Project p);
+
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
index 8b8e523f..87999ec8 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -1,7 +1,10 @@
 package org.eclipsefoundation.git.eca.service.impl;
 
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
+import java.util.Map.Entry;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -14,13 +17,17 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.AccountsAPI;
+import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.service.OAuthService;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.databind.JsonNode;
+
 /**
  * Wrapped cached and authenticated access to user objects.
  * 
@@ -39,6 +46,9 @@ public class CachedUserService implements UserService {
     @Inject
     @RestClient
     AccountsAPI accounts;
+    @Inject
+    @RestClient
+    BotsAPI bots;
 
     @Inject
     OAuthService oauth;
@@ -56,7 +66,8 @@ public class CachedUserService implements UserService {
 
     @Override
     public EclipseUser getUser(String mail) {
-        Optional<EclipseUser> u =cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(mail));
+        Optional<EclipseUser> u = cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class,
+                () -> retrieveUser(mail));
         if (u.isPresent()) {
             LOGGER.debug("Found user with email {}", mail);
             return u.get();
@@ -77,6 +88,32 @@ public class CachedUserService implements UserService {
         return null;
     }
 
+    @Override
+    public boolean userIsABot(String mail, List<Project> filteredProjects) {
+        if (mail == null || "".equals(mail.trim())) {
+            return false;
+        }
+        List<JsonNode> botObjs = getBots();
+        // if there are no matching projects, then check against all bots, not just
+        // project bots
+        if (filteredProjects == null || filteredProjects.isEmpty()) {
+            return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
+        }
+        // for each of the matched projects, check the bot for the matching project ID
+        for (Project p : filteredProjects) {
+            LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
+            for (JsonNode bot : botObjs) {
+                // if the project ID match, and one of the email fields matches, then user is
+                // bot
+                if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
+                        && checkFieldsForMatchingMail(bot, mail)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Checks for standard and noreply email address matches for a Git user and converts to a Eclipse Foundation account
      * object.
@@ -143,6 +180,47 @@ public class CachedUserService implements UserService {
         return null;
     }
 
+    /**
+     * Checks JSON node to look for email fields, both at the root, and nested email fields.
+     * 
+     * @param bot the bots JSON object representation
+     * @param mail the email to match against
+     * @return true if the bot has a matching email value, otherwise false
+     */
+    private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
+        // check the root email for the bot for match
+        JsonNode botmail = bot.get("email");
+        if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
+            LOGGER.debug("Found matching bot at root level for '{}'", mail);
+            return true;
+        }
+        Iterator<Entry<String, JsonNode>> i = bot.fields();
+        while (i.hasNext()) {
+            Entry<String, JsonNode> e = i.next();
+            // check that our field is an object with fields
+            JsonNode node = e.getValue();
+            if (node.isObject()) {
+                LOGGER.debug("Checking {} for bot email", e.getKey());
+                // if the mail matches (ignoring case) user is bot
+                JsonNode botAliasMail = node.get("email");
+                if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
+                    LOGGER.debug("Found match for bot email {}", mail);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private List<JsonNode> getBots() {
+        Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,
+                () -> bots.getBots());
+        if (!allBots.isPresent()) {
+            return Collections.emptyList();
+        }
+        return allBots.get();
+    }
+
     private String getBearerToken() {
         return "Bearer " + oauth.getToken();
     }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
new file mode 100644
index 00000000..b443950b
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -0,0 +1,110 @@
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.helper.CommitHelper;
+import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.CommitStatus;
+import org.eclipsefoundation.git.eca.model.Project;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.model.ValidationResponse;
+import org.eclipsefoundation.git.eca.service.ValidationService;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.service.FilterService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class DefaultValidationService implements ValidationService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValidationService.class);
+
+    @Inject
+    PersistenceDao dao;
+    @Inject
+    FilterService filters;
+
+    @Override
+    public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public List<CommitValidationStatus> getRequestCommitValidationStatus(RequestWrapper wrapper, ValidationRequest req,
+            String projectId) {
+        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
+                CommitHelper.getCommitParams(req, projectId));
+        // set use limit to false to collect all data in one request
+        q.setUseLimit(false);
+        return dao.get(q);
+    }
+
+    @Override
+    public void updateCommitValidationStatus(RequestWrapper wrapper, ValidationResponse r, ValidationRequest req,
+            List<CommitValidationStatus> statuses, Project p) {
+        // remove existing messaging
+        RDBMSQuery<CommitValidationMessage> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class),
+                CommitHelper.getCommitParams(req, p != null ? p.getProjectId() : null));
+
+        List<CommitValidationMessage> messages = new ArrayList<>();
+        List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
+        for (Entry<String, CommitStatus> e : r.getCommits().entrySet()) {
+            if (ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())) {
+                LOGGER.warn("Not logging errors/validation associated with unknown commit");
+                continue;
+            }
+            // update the status if present, otherwise make new one.
+            Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getSha()))
+                    .findFirst();
+            CommitValidationStatus base;
+            if (status.isPresent()) {
+                base = status.get();
+            } else {
+                base = new CommitValidationStatus();
+                base.setProject(p != null ? p.getProjectId() : null);
+                base.setSha(e.getKey());
+                base.setProvider(req.getProvider());
+                base.setRepoUrl(req.getRepoUrl().toString());
+                base.setCreationDate(DateTimeHelper.now());
+            }
+            base.setLastModified(DateTimeHelper.now());
+            updatedStatuses.add(base);
+            // get the commit for current status
+            Optional<Commit> commit = req.getCommits().stream().filter(c -> c.getHash().equals(e.getKey())).findFirst();
+            if (commit.isEmpty()) {
+                // TODO
+            }
+            Commit c = commit.get();
+            // if there are errors, update validation messages
+            if (e.getValue().getErrors().size() > 0) {
+                messages.addAll(e.getValue().getErrors().stream().map(err -> {
+                    CommitValidationMessage m = new CommitValidationMessage();
+                    m.setAuthorEmail(c.getAuthor().getMail());
+                    m.setStatusCode(err.getCode().getValue());
+                    // TODO add a checked way to set this
+                    m.setEclipseId(null);
+                    m.setProviderId(null);
+                    m.setCommit(base);
+                    return m;
+                }).collect(Collectors.toList()));
+            }
+        }
+        // update the base commit status and messages
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
+        // if present, remove any current validation messages as we will be pushing new ones
+        dao.delete(q);
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
+    }
+}
-- 
GitLab


From 67556949ebf78a745548ef580d87a4bed321f766 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 18 May 2022 14:51:12 -0400
Subject: [PATCH 04/10] Fix local GL instance created by docker-compose using
 wrong host

---
 docker-compose.yaml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docker-compose.yaml b/docker-compose.yaml
index 724e9a63..656341eb 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -9,7 +9,8 @@ services:
       VIRTUAL_PORT: 443
       VIRTUAL_PROTO: https
       CERT_NAME: dev.docker
-  shm_size: '256m'
+      GITLAB_OMNIBUS_CONFIG: "external_url 'http://localhost/';"
+    shm_size: '256m'
     ports:
       - 443:443
       - 80:80
-- 
GitLab


From 58f59af919045cd77ad2a555639dec20ff3ed804 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 18 May 2022 16:05:16 -0400
Subject: [PATCH 05/10] Fix tests broken from refactoring of users logic

---
 .../eca/resource/ValidationResourceTest.java  | 19 +++++++++++++++++++
 .../test/service/impl/MockUserService.java    | 10 ----------
 2 files changed, 19 insertions(+), 10 deletions(-)
 delete mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java

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 907379dc..2103c05b 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -101,6 +101,25 @@ class ValidationResourceTest {
                 .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
                         .setName("grunter")
                         .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+
+        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("1.bot@eclipse.org"),
+                ArgumentMatchers
+                        .argThat(l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("sample.proj")))))
+                .thenReturn(true);
+        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("2.bot@eclipse.org"),
+                ArgumentMatchers.argThat(
+                        l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("sample.proto")))))
+                .thenReturn(true);
+        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("2.bot-github@eclipse.org"),
+                ArgumentMatchers.argThat(l -> l.stream().anyMatch(p -> p.getProjectId().equals("sample.proto")))))
+                .thenReturn(true);
+        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("3.bot@eclipse.org"),
+                ArgumentMatchers
+                        .argThat(l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("spec.proj")))))
+                .thenReturn(true);
+        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("3.bot-gitlab@eclipse.org"),
+                ArgumentMatchers.argThat(l -> l.stream().anyMatch(p -> p.getProjectId().equals("spec.proj")))))
+                .thenReturn(true);
         // if dev servers are run on the same machine, some values may live in the cache
         cs.removeAll();
     }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java
deleted file mode 100644
index c799f363..00000000
--- a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.eclipsefoundation.git.eca.test.service.impl;
-
-import javax.enterprise.context.ApplicationScoped;
-
-import org.eclipsefoundation.git.eca.service.impl.CachedUserService;
-
-public class MockUserService extends CachedUserService {
-
-    
-}
-- 
GitLab


From 20ef017c39aca3db3a1eb20285509ea634793193 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 19 May 2022 15:59:04 -0400
Subject: [PATCH 06/10] Add commit groups and basic UI for displaying commit
 groups

Commit group fingerprints will be added to the validation response in
the following commit. Additionally, tests will be authored to test new
services and work flows as well.
---
 pom.xml                                       |   4 +
 .../git/eca/dto/CommitValidationStatus.java   |   2 +-
 .../dto/CommitValidationStatusGrouping.java   | 126 ++++++++++++++++++
 .../eca/namespace/GitEcaParameterNames.java   |   2 +
 .../git/eca/resource/ValidationResource.java  |  17 ++-
 .../impl/DefaultValidationService.java        |  40 +++++-
 .../resources/templates/eclipse_footer.html   |  62 +++++++++
 .../resources/templates/eclipse_header.html   |  90 +++++++++++++
 .../templates/simple_fingerprint_ui.html      |  51 +++++++
 9 files changed, 389 insertions(+), 5 deletions(-)
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
 create mode 100644 src/main/resources/templates/eclipse_footer.html
 create mode 100644 src/main/resources/templates/eclipse_header.html
 create mode 100644 src/main/resources/templates/simple_fingerprint_ui.html

diff --git a/pom.xml b/pom.xml
index 40e7405f..9fa683a1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,10 @@
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-rest-client</artifactId>
     </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-resteasy-qute</artifactId>
+    </dependency>
     <!-- Annotation preprocessors - reduce all of the boiler plate -->
     <dependency>
       <groupId>com.google.auto.value</groupId>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
index 7cc003c6..9fb1c844 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
@@ -43,7 +43,7 @@ public class CommitValidationStatus extends BareNode {
     private List<CommitValidationMessage> errors;
 
     @Override
-    public Object getId() {
+    public Integer getId() {
         return id;
     }
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
new file mode 100644
index 00000000..5c664c90
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
@@ -0,0 +1,126 @@
+package org.eclipsefoundation.git.eca.dto;
+
+import java.io.Serializable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.persistence.Embeddable;
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+@Entity
+@Table
+public class CommitValidationStatusGrouping extends BareNode {
+    public static final DtoTable TABLE = new DtoTable(CommitValidationStatusGrouping.class, "cvsg");
+
+    @EmbeddedId
+    private GroupingCompositeId compositeId;
+
+    public CommitValidationStatusGrouping() {
+    }
+
+    public CommitValidationStatusGrouping(String fingerprint, CommitValidationStatus commit) {
+        this.compositeId = new GroupingCompositeId();
+        this.compositeId.setFingerprint(fingerprint);
+        this.compositeId.setCommit(commit);
+    }
+
+    @Override
+    public GroupingCompositeId getId() {
+        return compositeId;
+    }
+
+    /**
+     * @return the compositeId
+     */
+    public GroupingCompositeId getCompositeId() {
+        return compositeId;
+    }
+
+    /**
+     * @param compositeId the compositeId to set
+     */
+    public void setCompositeId(GroupingCompositeId compositeId) {
+        this.compositeId = compositeId;
+    }
+
+    @Embeddable
+    public static class GroupingCompositeId implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        private String fingerprint;
+        @OneToOne
+        private CommitValidationStatus commit;
+
+        /**
+         * @return the fingerprint
+         */
+        public String getFingerprint() {
+            return fingerprint;
+        }
+
+        /**
+         * @param fingerprint the fingerprint to set
+         */
+        public void setFingerprint(String fingerprint) {
+            this.fingerprint = fingerprint;
+        }
+
+        /**
+         * @return the commit
+         */
+        public CommitValidationStatus getCommit() {
+            return commit;
+        }
+
+        /**
+         * @param commit the commit to set
+         */
+        public void setCommit(CommitValidationStatus commit) {
+            this.commit = commit;
+        }
+
+    }
+
+    @Singleton
+    public static class CommitValidationStatusGroupingFilter implements DtoFilter<CommitValidationStatusGrouping> {
+        @Inject
+        ParameterizedSQLStatementBuilder builder;
+
+        @Override
+        public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
+            ParameterizedSQLStatement stmt = builder.build(TABLE);
+            if (isRoot) {
+                // fingerprint check
+                String fingerprint = params.getFirst(GitEcaParameterNames.FINGERPRINT.getName());
+                if (StringUtils.isNumeric(fingerprint)) {
+                    stmt.addClause(new ParameterizedSQLStatement.Clause(
+                            TABLE.getAlias() + ".compositeId.fingerprint = ?", new Object[] { fingerprint }));
+                }
+                // commit id check
+                String commitId = params.getFirst(GitEcaParameterNames.COMMIT_ID.getName());
+                if (StringUtils.isNumeric(commitId)) {
+                    stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".compositeId.commit.id = ?",
+                            new Object[] { Integer.valueOf(commitId) }));
+                }
+            }
+            return stmt;
+        }
+
+        @Override
+        public Class<CommitValidationStatusGrouping> getType() {
+            return CommitValidationStatusGrouping.class;
+        }
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
index 880aea77..e11e0bec 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -14,11 +14,13 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final String SHAS_RAW = "shas";
     public static final String PROJECT_ID_RAW = "project_id";
     public static final String REPO_URL_RAW = "repo_url";
+    public static final String FINGERPRINT_RAW = "commit_id";
     public static final UrlParameter COMMIT_ID = new UrlParameter(COMMIT_ID_RAW);
     public static final UrlParameter SHA = new UrlParameter(SHA_RAW);
     public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW);
     public static final UrlParameter PROJECT_ID = new UrlParameter(PROJECT_ID_RAW);
     public static final UrlParameter REPO_URL = new UrlParameter(REPO_URL_RAW);
+    public static final UrlParameter FINGERPRINT = new UrlParameter(FINGERPRINT_RAW);
 
     @Override
     public List<UrlParameter> getParameters() {
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 f445a133..4f4606b9 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -45,6 +45,9 @@ import org.eclipsefoundation.git.eca.service.ValidationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.quarkus.qute.Location;
+import io.quarkus.qute.Template;
+
 /**
  * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate
  * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request
@@ -78,6 +81,10 @@ public class ValidationResource {
     @Inject
     ValidationService validation;
 
+    // Qute templates, generates email bodies
+    @Location("simple_fingerprint_ui")
+    Template membershipTemplateWeb;
+
     /**
      * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository
      * provider. These commits will be validated to ensure that all users are covered either by an ECA, or are
@@ -141,8 +148,14 @@ public class ValidationResource {
     @GET
     @Produces(MediaType.TEXT_HTML)
     @Path("status/{fingerprint}/ui")
-    public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
-        return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build();
+    public String getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
+        List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint);
+        if (statuses.isEmpty()) {
+            // todo
+            return "";
+        }
+
+        return membershipTemplateWeb.data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl()).render();
     }
 
     /**
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
index b443950b..f3b06fb3 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -1,5 +1,7 @@
 package org.eclipsefoundation.git.eca.service.impl;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map.Entry;
@@ -8,24 +10,30 @@ import java.util.stream.Collectors;
 
 import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
+import javax.ws.rs.core.MultivaluedMap;
 
 import org.eclipsefoundation.core.helper.DateTimeHelper;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatusGrouping;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.CommitStatus;
 import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.git.eca.service.ValidationService;
 import org.eclipsefoundation.persistence.dao.PersistenceDao;
 import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.eclipsefoundation.persistence.service.FilterService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.undertow.util.HexConverter;
+
 @ApplicationScoped
 public class DefaultValidationService implements ValidationService {
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValidationService.class);
@@ -37,8 +45,14 @@ public class DefaultValidationService implements ValidationService {
 
     @Override
     public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint) {
-        // TODO Auto-generated method stub
-        return null;
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.add(GitEcaParameterNames.FINGERPRINT_RAW, fingerprint);
+        RDBMSQuery<CommitValidationStatusGrouping> q = new RDBMSQuery<>(wrapper,
+                filters.get(CommitValidationStatusGrouping.class), params);
+        // set use limit to false to collect all data in one request
+        q.setUseLimit(false);
+        return dao.get(q).stream().map(statusGrouping -> statusGrouping.getCompositeId().getCommit())
+                .collect(Collectors.toList());
     }
 
     @Override
@@ -101,10 +115,32 @@ public class DefaultValidationService implements ValidationService {
                 }).collect(Collectors.toList()));
             }
         }
+        String fingerprint = generateRequestHash(req);
         // update the base commit status and messages
         dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class)), updatedStatuses);
+        dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatusGrouping.class)), updatedStatuses.stream()
+                .map(s -> new CommitValidationStatusGrouping(fingerprint, s)).collect(Collectors.toList()));
         // if present, remove any current validation messages as we will be pushing new ones
         dao.delete(q);
         dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
     }
+
+    /**
+     * Generates a request fingerprint for looking up requests that have already been processed in the past. Collision
+     * here is extremely unlikely, and low risk on the change it does. For that reason, a more secure but heavier
+     * hashing alg. wasn't chosen.
+     * 
+     * @param req the request to generate a fingerprint for
+     * @return the fingerprint for the request
+     */
+    private String generateRequestHash(ValidationRequest req) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(req.getRepoUrl());
+        req.getCommits().forEach(c -> sb.append(c.getHash()));
+        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.");
+        }
+    }
 }
diff --git a/src/main/resources/templates/eclipse_footer.html b/src/main/resources/templates/eclipse_footer.html
new file mode 100644
index 00000000..e9d84704
--- /dev/null
+++ b/src/main/resources/templates/eclipse_footer.html
@@ -0,0 +1,62 @@
+
+<p id="back-to-top">
+  <a class="visible-xs" href="#top">Back to the top</a>
+</p>
+  <div class="eclipsefdn-featured-story featured-footer" data-publish-target="eclipse_org"><div class="container featured-container"></div></div><footer id="solstice-footer">
+    <div class="container">
+    <div class="row">
+      <section class="col-sm-6 hidden-print" id="footer-eclipse-foundation">
+            <h2 class="section-title">Eclipse Foundation</h2>
+    <ul class="nav"><li><a href="https://www.eclipse.org/org/">About Us</a></li><li><a href="https://www.eclipse.org/org/foundation/contact.php">Contact Us</a></li><li><a href="https://www.eclipse.org/donate">Donate</a></li><li><a href="https://www.eclipse.org/membership/">Members</a></li><li><a href="https://www.eclipse.org/org/documents/">Governance</a></li><li><a href="https://www.eclipse.org/org/documents/Community_Code_of_Conduct.php">Code of Conduct</a></li><li><a href="https://www.eclipse.org/artwork/">Logo and Artwork</a></li><li><a href="https://www.eclipse.org/org/foundation/directors.php">Board of Directors</a></li></ul>      </section>
+      <section class="col-sm-6 hidden-print" id="footer-legal">
+            <h2 class="section-title">Legal</h2>
+    <ul class="nav"><li><a href="https://www.eclipse.org/legal/privacy.php">Privacy Policy</a></li><li><a href="https://www.eclipse.org/legal/termsofuse.php">Terms of Use</a></li><li><a href="https://www.eclipse.org/legal/copyright.php">Copyright Agent</a></li><li><a href="https://www.eclipse.org/legal/epl-2.0/">Eclipse Public License</a></li><li><a href="https://www.eclipse.org/legal/">Legal Resources</a></li></ul>      </section>
+      <section class="col-sm-6 hidden-print" id="footer-useful-links">
+            <h2 class="section-title">Useful Links</h2>
+    <ul class="nav"><li><a href="https://bugs.eclipse.org/bugs/">Report a Bug</a></li><li><a href="//help.eclipse.org/">Documentation</a></li><li><a href="https://www.eclipse.org/contribute/">How to Contribute</a></li><li><a href="https://www.eclipse.org/mail/">Mailing Lists</a></li><li><a href="https://www.eclipse.org/forums/">Forums</a></li><li><a href="//marketplace.eclipse.org">Marketplace</a></li></ul>      </section>
+      <section class="col-sm-6 hidden-print" id="footer-other">
+            <h2 class="section-title">Other</h2>
+    <ul class="nav"><li><a href="https://www.eclipse.org/ide/">IDE and Tools</a></li><li><a href="https://www.eclipse.org/projects">Projects</a></li><li><a href="https://www.eclipse.org/org/workinggroups/">Working Groups</a></li><li><a href="https://www.eclipse.org/org/research/">Research@Eclipse</a></li><li><a href="https://www.eclipse.org/security/">Report a Vulnerability</a></li><li><a href="https://status.eclipse.org">Service Status</a></li></ul>      </section>
+            <div class="col-sm-24 margin-top-20">
+        <div class="row">
+          <div id="copyright" class="col-md-16">
+            <p id="copyright-text">Copyright &copy; Eclipse Foundation. All Rights Reserved.</p>
+          </div>
+          <div class="col-md-8 social-media">
+            <ul class="list-inline">
+              <li>
+                <a class="social-media-link fa-stack fa-lg" href="https://twitter.com/EclipseFdn" aria-label="Eclipse Foundation Twitter profile">
+                  <i class="fa fa-circle-thin fa-stack-2x"></i>
+                  <i class="fa fa-twitter fa-stack-1x"></i>
+                </a>
+              </li>
+              <li>
+                <a class="social-media-link fa-stack fa-lg" href="https://www.facebook.com/eclipse.org" aria-label="Eclipse Foundation Facebook page">
+                  <i class="fa fa-circle-thin fa-stack-2x"></i>
+                  <i class="fa fa-facebook fa-stack-1x"></i>
+                </a>
+              </li>
+              <li>
+                <a class="social-media-link fa-stack fa-lg" href="https://www.youtube.com/user/EclipseFdn" aria-label="Eclipse Foundation YouTube channel">
+                  <i class="fa fa-circle-thin fa-stack-2x"></i>
+                  <i class="fa fa-youtube fa-stack-1x"></i>
+                </a>
+              </li>
+              <li>
+                <a class="social-media-link fa-stack fa-lg" href="https://www.linkedin.com/company/eclipse-foundation" aria-label="Eclipse Foundation Linkedin profile">
+                  <i class="fa fa-circle-thin fa-stack-2x"></i>
+                  <i class="fa fa-linkedin fa-stack-1x"></i>
+                </a>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>      <a href="#" class="scrollup">Back to the top</a>
+    </div>
+  </div>
+</footer>
+<!-- Placed at the end of the document so the pages load faster -->
+<script src="//www.eclipse.org/eclipse.org-common/themes/solstice/public/javascript/main.min.js?var=0.0.178"></script>
+
+</body>
+</html>
diff --git a/src/main/resources/templates/eclipse_header.html b/src/main/resources/templates/eclipse_header.html
new file mode 100644
index 00000000..b03554d8
--- /dev/null
+++ b/src/main/resources/templates/eclipse_header.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <!-- Google Tag Manager -->
+{|<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
+new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
+j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
+'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+})(window,document,'script','dataLayer','GTM-5WLCZXC');</script>|}
+<!-- End Google Tag Manager -->    <meta name="author" content="Christopher Guindon"/>
+    <meta name="keywords" content="eclipse.org, Eclipse Foundation"/>
+    <link rel="shortcut icon" href="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/favicon.ico"/>
+    <title>HTML Template | The Eclipse Foundation</title>
+    <link rel="preconnect stylesheet" href="//www.eclipse.org/eclipse.org-common/themes/solstice/public/stylesheets/quicksilver.min.css?v0.180"/>
+<meta name="description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/>
+<meta property="og:description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/>
+<meta property="og:image" content="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-200x200.png"/>
+<meta property="og:title" content="HTML Template | The Eclipse Foundation"/>
+<meta property="og:image:width" content="200"/>
+<meta property="og:image:height" content="200"/>
+<meta itemprop="name" content="HTML Template | The Eclipse Foundation"/>
+<meta itemprop="description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/>
+<meta itemprop="image" content="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-400x400.png"/>
+<meta name="twitter:site" content="@EclipseFdn"/>
+<meta name="twitter:card" content="summary"/>
+<meta name="twitter:title" content="HTML Template | The Eclipse Foundation"/>
+<meta name="twitter:url" content="https://www.eclipse.org/eclipse.org-common/themes/solstice/html_template/index.php?theme=default&layout=default-header"/>
+<meta name="twitter:description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/>
+<meta name="twitter:image" content="https://www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-400x400.png"/>
+<link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="preconnect stylesheet" type="text/css"/>
+    {|<script> var eclipse_org_common = {"settings":{"cookies_class":{"name":"eclipse_settings","enabled":1}}}</script>|}  </head>
+  <body id="body_solstice">
+    <!-- Google Tag Manager (noscript) -->
+<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5WLCZXC"
+height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
+<!-- End Google Tag Manager (noscript) -->    <a class="sr-only" href="#content">Skip to main content</a>
+    <header class="header-wrapper" id="header-wrapper">
+      <div class="clearfix toolbar-container-wrapper">
+      <div class="container">
+        <div class="text-right toolbar-row row hidden-print">
+          <div class="col-md-24 row-toolbar-col">
+            <ul class="list-inline">
+              <li><a class="toolbar-link" href="https://accounts.eclipse.org/user/login/?takemeback=https%3A%2F%2Fwww.eclipse.org%2Feclipse.org-common%2Fthemes%2Fsolstice%2Fhtml_template%2Findex.php"><i class="fa fa-sign-in"></i> Log in</a></li>
+              <li><a class="toolbar-link toolbar-manage-cookies dropdown-toggle"><i class="fa fa-wrench"></i> Manage Cookies</a></li>
+            </ul>
+          </div>
+          
+        </div>
+      </div>
+    </div>  <div class="container">
+    <div class="row" id="header-row">
+            <div class="col-sm-5 col-md-4" id="header-left">
+        <div class="wrapper-logo-default"><a href="https://www.eclipse.org/"><img class="logo-eclipse-default hidden-xs" alt="Eclipse.org logo" width="160" src="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-white-orange.svg"/></a></div>
+      </div>            <div class="col-sm-19 col-md-20 margin-top-10" id="main-menu-wrapper">
+      <div class="float-right hidden-xs" id="btn-call-for-action"><a href="https://www.eclipse.org/donate/" class="btn btn-huge btn-info"><i class="fa fa-star"></i> Donate</a></div>    <div class="navbar yamm float-sm-right" id="main-menu">
+    <div class="navbar-collapse collapse" id="navbar-main-menu">
+      <ul class="nav navbar-nav">
+        <li><a href="https://www.eclipse.org/projects/" target="_self">Projects</a></li><li><a href="https://www.eclipse.org/org/workinggroups/" target="_self">Working Groups</a></li><li><a href="https://www.eclipse.org/membership/" target="_self">Members</a></li>                  <li class="dropdown visible-xs"><a href="#" data-toggle="dropdown" class="dropdown-toggle">Community <b class="caret"></b></a><ul class="dropdown-menu"><li><a href="http://marketplace.eclipse.org">Marketplace</a></li><li><a href="http://events.eclipse.org">Events</a></li><li><a href="http://www.planeteclipse.org/">Planet Eclipse</a></li><li><a href="https://www.eclipse.org/community/eclipse_newsletter/">Newsletter</a></li><li><a href="https://www.youtube.com/user/EclipseFdn">Videos</a></li><li><a href="https://blogs.eclipse.org">Blogs</a></li></ul></li><li class="dropdown visible-xs"><a href="#" data-toggle="dropdown" class="dropdown-toggle">Participate <b class="caret"></b></a><ul class="dropdown-menu"><li><a href="https://bugs.eclipse.org/bugs/">Report a Bug</a></li><li><a href="https://www.eclipse.org/forums/">Forums</a></li><li><a href="https://www.eclipse.org/mail/">Mailing Lists</a></li><li><a href="https://wiki.eclipse.org/">Wiki</a></li><li><a href="https://wiki.eclipse.org/IRC">IRC</a></li><li><a href="https://www.eclipse.org/org/research/">Research</a></li></ul></li><li class="dropdown visible-xs"><a href="#" data-toggle="dropdown" class="dropdown-toggle">Eclipse IDE <b class="caret"></b></a><ul class="dropdown-menu"><li><a href="https://www.eclipse.org/downloads">Download</a></li><li><a href="https://www.eclipse.org/eclipseide">Learn More</a></li><li><a href="https://help.eclipse.org">Documentation</a></li><li><a href="https://www.eclipse.org/getting_started">Getting Started / Support</a></li><li><a href="https://www.eclipse.org/contribute/">How to Contribute</a></li><li><a href="https://www.eclipse.org/ide/">IDE and Tools</a></li><li><a href="https://www.eclipse.org/forums/index.php/f/89/">Newcomer Forum</a></li></ul></li>          <!-- More -->
+          <li class="dropdown eclipse-more hidden-xs">
+            <a data-toggle="dropdown" class="dropdown-toggle" role="button">More<b class="caret"></b></a>
+            <ul class="dropdown-menu">
+              <li>
+                <!-- Content container to add padding -->
+                <div class="yamm-content">
+                  <div class="row">
+                    <ul class="col-sm-8 list-unstyled"><li><p><strong>Community</strong></p></li><li><a href="http://marketplace.eclipse.org">Marketplace</a></li><li><a href="http://events.eclipse.org">Events</a></li><li><a href="http://www.planeteclipse.org/">Planet Eclipse</a></li><li><a href="https://www.eclipse.org/community/eclipse_newsletter/">Newsletter</a></li><li><a href="https://www.youtube.com/user/EclipseFdn">Videos</a></li><li><a href="https://blogs.eclipse.org">Blogs</a></li></ul><ul class="col-sm-8 list-unstyled"><li><p><strong>Participate</strong></p></li><li><a href="https://bugs.eclipse.org/bugs/">Report a Bug</a></li><li><a href="https://www.eclipse.org/forums/">Forums</a></li><li><a href="https://www.eclipse.org/mail/">Mailing Lists</a></li><li><a href="https://wiki.eclipse.org/">Wiki</a></li><li><a href="https://wiki.eclipse.org/IRC">IRC</a></li><li><a href="https://www.eclipse.org/org/research/">Research</a></li></ul><ul class="col-sm-8 list-unstyled"><li><p><strong>Eclipse IDE</strong></p></li><li><a href="https://www.eclipse.org/downloads">Download</a></li><li><a href="https://www.eclipse.org/eclipseide">Learn More</a></li><li><a href="https://help.eclipse.org">Documentation</a></li><li><a href="https://www.eclipse.org/getting_started">Getting Started / Support</a></li><li><a href="https://www.eclipse.org/contribute/">How to Contribute</a></li><li><a href="https://www.eclipse.org/ide/">IDE and Tools</a></li><li><a href="https://www.eclipse.org/forums/index.php/f/89/">Newcomer Forum</a></li></ul>                  </div>
+                </div>
+              </li>
+            </ul>
+          </li>
+                
+      </ul>
+    </div>
+    <div class="navbar-header">
+      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-main-menu">
+      <span class="sr-only">Toggle navigation</span>
+      <span class="icon-bar"></span>
+      <span class="icon-bar"></span>
+      <span class="icon-bar"></span>
+      <span class="icon-bar"></span>
+      </button>
+      <div class="wrapper-logo-mobile"><a class="navbar-brand visible-xs" href="https://www.eclipse.org/"><img class="logo-eclipse-default-mobile img-responsive" alt="Eclipse.org logo" width="160" src="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-white-orange.svg"/></a></div>    </div>
+  </div>
+</div>
+    </div>
+  </div>
+  </header>
\ No newline at end of file
diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html
new file mode 100644
index 00000000..dc3851c4
--- /dev/null
+++ b/src/main/resources/templates/simple_fingerprint_ui.html
@@ -0,0 +1,51 @@
+{#include eclipse_header /}
+{|<style>
+.panel.list-group > div {
+    background-color: white;
+    border-left: 1px solid #ddd;
+    border-right: 1px solid #ddd;
+}
+.panel.list-group .list-group-item .badge {
+    background-color: #ffd8cc;
+    color: #c74922;
+    border-color: #c74922;
+}</style>|}
+<section class="container">
+  <h1>Repo: {repoUrl}</h1>
+  <div id="accordion">
+    <div class="panel list-group">
+      {#for status in statuses.orEmpty}
+      <a href="#{status.sha}" data-parent="#accordion" data-toggle="collapse" class="list-group-item">
+      {#if status.errors.size > 0}
+      <span class="badge">{status.errors.size}</span>
+      {/if}
+        <p>SHA: <strong>{status.sha}</strong></p>
+      </a>
+      
+      <div class="padding-30 collapse" id="{status.sha}">
+        <div>Last validated: <em>{status.lastModified.format('d MMM uuuu')}</em></div>
+        <div>Errors: <em>{status.errors.size}</em></div>
+        <ul class="list-group-item-text">
+          {#for error in status.errors.orEmpty}
+            {#if error.statusCode == -406}
+            <li>Committer does not have permission to push on behalf of another user (Legacy).</li>
+            {#else if error.statusCode == -405}
+            <li>Committer did not have a signed ECA on file.</li>
+            {#else if error.statusCode == -404}
+            <li>Author did not have a signed ECA on file.</li>
+            {#else if error.statusCode == -403}
+            <li>Committer does not have permission to commit on specification projects.</li>
+            {#else if error.statusCode == -402}
+            <li>Sign-off not detected in the commit message (Legacy).</li>
+            {#else if error.statusCode == -401}
+            <li>Request format/state error detected.</li>
+            {#else}
+            <li>Unaccounted for error detected, please contact administrators.</li>
+            {/if}
+          {/for}
+        </ul>
+      </div>
+      {/for}
+  </div>
+</section>
+{#include eclipse_footer /}
\ No newline at end of file
-- 
GitLab


From edd3b8ebd39d9ce626cd87cd39521af662141895 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Tue, 24 May 2022 12:52:10 -0400
Subject: [PATCH 07/10] Update testing and test API stubs, fix minor issues w/
 commit groups

---
 .../dto/CommitValidationStatusGrouping.java   |   2 +-
 .../eca/namespace/GitEcaParameterNames.java   |   2 +-
 .../git/eca/resource/ValidationResource.java  |  10 +-
 .../git/eca/service/ValidationService.java    |  23 +++
 .../impl/DefaultValidationService.java        |  28 +--
 .../eca/resource/ValidationResourceTest.java  |  63 -------
 .../service/impl/CachedUserServiceTest.java   | 131 +++++++++++++
 .../impl/DefaultValidationServiceTest.java    | 176 ++++++++++++++++++
 .../git/eca/test/api/MockAccountsAPI.java     |  69 +++++++
 .../database/default/V1.0.0__default.sql      |  12 +-
 10 files changed, 421 insertions(+), 95 deletions(-)
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationServiceTest.java
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java

diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
index 5c664c90..4441c2fb 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
@@ -104,7 +104,7 @@ public class CommitValidationStatusGrouping extends BareNode {
             if (isRoot) {
                 // fingerprint check
                 String fingerprint = params.getFirst(GitEcaParameterNames.FINGERPRINT.getName());
-                if (StringUtils.isNumeric(fingerprint)) {
+                if (StringUtils.isNotBlank(fingerprint)) {
                     stmt.addClause(new ParameterizedSQLStatement.Clause(
                             TABLE.getAlias() + ".compositeId.fingerprint = ?", new Object[] { fingerprint }));
                 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
index e11e0bec..c114c7a4 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -14,7 +14,7 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final String SHAS_RAW = "shas";
     public static final String PROJECT_ID_RAW = "project_id";
     public static final String REPO_URL_RAW = "repo_url";
-    public static final String FINGERPRINT_RAW = "commit_id";
+    public static final String FINGERPRINT_RAW = "fingerprint";
     public static final UrlParameter COMMIT_ID = new UrlParameter(COMMIT_ID_RAW);
     public static final UrlParameter SHA = new UrlParameter(SHA_RAW);
     public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW);
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 4f4606b9..5e83555f 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -148,14 +148,14 @@ public class ValidationResource {
     @GET
     @Produces(MediaType.TEXT_HTML)
     @Path("status/{fingerprint}/ui")
-    public String getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
+    public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
         List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint);
         if (statuses.isEmpty()) {
-            // todo
-            return "";
+            return Response.status(404).build();
         }
-
-        return membershipTemplateWeb.data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl()).render();
+        return Response.ok().entity(
+                membershipTemplateWeb.data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl()).render())
+                .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 0c7779a8..de9d9dec 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
@@ -1,5 +1,7 @@
 package org.eclipsefoundation.git.eca.service;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 
 import org.eclipsefoundation.core.model.RequestWrapper;
@@ -8,6 +10,8 @@ import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 
+import io.undertow.util.HexConverter;
+
 /**
  * 
  * @author martin
@@ -49,4 +53,23 @@ public interface ValidationService {
     public void updateCommitValidationStatus(RequestWrapper wrapper, ValidationResponse r, ValidationRequest req,
             List<CommitValidationStatus> statuses, Project p);
 
+
+    /**
+     * Generates a request fingerprint for looking up requests that have already been processed in the past. Collision
+     * here is extremely unlikely, and low risk on the change it does. For that reason, a more secure but heavier
+     * hashing alg. wasn't chosen.
+     * 
+     * @param req the request to generate a fingerprint for
+     * @return the fingerprint for the request
+     */
+    default String generateRequestHash(ValidationRequest req) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(req.getRepoUrl());
+        req.getCommits().forEach(c -> sb.append(c.getHash()));
+        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.");
+        }
+    }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
index f3b06fb3..b264525e 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -1,8 +1,7 @@
 package org.eclipsefoundation.git.eca.service.impl;
 
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Optional;
@@ -12,6 +11,7 @@ import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
 import javax.ws.rs.core.MultivaluedMap;
 
+import org.apache.commons.lang3.StringUtils;
 import org.eclipsefoundation.core.helper.DateTimeHelper;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
@@ -32,8 +32,6 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.undertow.util.HexConverter;
-
 @ApplicationScoped
 public class DefaultValidationService implements ValidationService {
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValidationService.class);
@@ -45,6 +43,9 @@ public class DefaultValidationService implements ValidationService {
 
     @Override
     public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint) {
+        if (StringUtils.isAllBlank(fingerprint)) {
+            return Collections.emptyList();
+        }
         MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
         params.add(GitEcaParameterNames.FINGERPRINT_RAW, fingerprint);
         RDBMSQuery<CommitValidationStatusGrouping> q = new RDBMSQuery<>(wrapper,
@@ -124,23 +125,4 @@ public class DefaultValidationService implements ValidationService {
         dao.delete(q);
         dao.add(new RDBMSQuery<>(wrapper, filters.get(CommitValidationMessage.class)), messages);
     }
-
-    /**
-     * Generates a request fingerprint for looking up requests that have already been processed in the past. Collision
-     * here is extremely unlikely, and low risk on the change it does. For that reason, a more secure but heavier
-     * hashing alg. wasn't chosen.
-     * 
-     * @param req the request to generate a fingerprint for
-     * @return the fingerprint for the request
-     */
-    private String generateRequestHash(ValidationRequest req) {
-        StringBuilder sb = new StringBuilder();
-        sb.append(req.getRepoUrl());
-        req.getCommits().forEach(c -> sb.append(c.getHash()));
-        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.");
-        }
-    }
 }
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 2103c05b..a92fb96a 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -25,25 +25,19 @@ import javax.inject.Inject;
 
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.model.Commit;
-import org.eclipsefoundation.git.eca.model.EclipseUser;
-import org.eclipsefoundation.git.eca.model.EclipseUser.ECA;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
-import org.eclipsefoundation.git.eca.service.impl.CachedUserService;
 import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Mockito;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
 import io.restassured.http.ContentType;
 
 /**
@@ -61,65 +55,8 @@ class ValidationResourceTest {
     @Inject
     ObjectMapper json;
 
-    @InjectMock
-    CachedUserService accounts;
-
     @BeforeEach
     void cacheClear() {
-        int id = 0;
-        // standard user fetches
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("newbie@important.co")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("newbie@important.co")
-                        .setName("newbieAnon").setECA(ECA.builder().build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("slom@eclipse-foundation.org")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
-                        .setMail("slom@eclipse-foundation.org").setName("barshall_blathers")
-                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("tester@eclipse-foundation.org")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
-                        .setMail("tester@eclipse-foundation.org").setName("mctesterson")
-                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("code.wiz@important.co")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("code.wiz@important.co")
-                        .setName("da_wizz")
-                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("grunt@important.co")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
-                        .setName("grunter")
-                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("paper.pusher@important.co")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
-                        .setMail("paper.pusher@important.co").setName("sumAnalyst")
-                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-
-        // gh user fetches
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("grunter@users.noreply.github.com")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
-                        .setName("grunter")
-                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-        Mockito.when(accounts.getUser(ArgumentMatchers.eq("123456789+grunter@users.noreply.github.com")))
-                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
-                        .setName("grunter")
-                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-
-        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("1.bot@eclipse.org"),
-                ArgumentMatchers
-                        .argThat(l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("sample.proj")))))
-                .thenReturn(true);
-        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("2.bot@eclipse.org"),
-                ArgumentMatchers.argThat(
-                        l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("sample.proto")))))
-                .thenReturn(true);
-        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("2.bot-github@eclipse.org"),
-                ArgumentMatchers.argThat(l -> l.stream().anyMatch(p -> p.getProjectId().equals("sample.proto")))))
-                .thenReturn(true);
-        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("3.bot@eclipse.org"),
-                ArgumentMatchers
-                        .argThat(l -> l.isEmpty() || l.stream().anyMatch(p -> p.getProjectId().equals("spec.proj")))))
-                .thenReturn(true);
-        Mockito.when(accounts.userIsABot(ArgumentMatchers.eq("3.bot-gitlab@eclipse.org"),
-                ArgumentMatchers.argThat(l -> l.stream().anyMatch(p -> p.getProjectId().equals("spec.proj")))))
-                .thenReturn(true);
         // if dev servers are run on the same machine, some values may live in the cache
         cs.removeAll();
     }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
new file mode 100644
index 00000000..6129c1b9
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
@@ -0,0 +1,131 @@
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.model.Project;
+import org.eclipsefoundation.git.eca.service.ProjectsService;
+import org.eclipsefoundation.git.eca.service.UserService;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+/**
+ * Tests user service impl using test stub data available from API stubs. While not a perfect test as there is no auth,
+ * it is good enough for unit testing.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@QuarkusTest
+public class CachedUserServiceTest {
+
+    @Inject
+    UserService users;
+    @Inject
+    ProjectsService projects;
+
+    @Test
+    void getUser_success() {
+        EclipseUser u = users.getUser("grunt@important.co");
+        // assert that this is the user we expect and that it exists
+        Assertions.assertTrue(u != null);
+        Assertions.assertTrue(u.getName().equals("grunter"));
+    }
+
+    @Test
+    void getUser_noReplyGH_success() {
+        EclipseUser u = users.getUser("123456789+grunter@users.noreply.github.com");
+        // assert that this is the user we expect and that it exists
+        Assertions.assertNotNull(u);
+        Assertions.assertTrue(u.getName().equals("grunter"));
+    }
+
+    @Test
+    void getUser_noReplyGH_noUser() {
+        Assertions.assertNull(users.getUser("123456789+farquad@users.noreply.github.com"));
+    }
+
+    @Test
+    void getUser_doesNotExist() {
+        Assertions.assertNull(users.getUser("nota.realboy@pinnochio.co"));
+    }
+
+    @Test
+    void getUser_nullOrEmptyEmail() {
+        Assertions.assertNull(users.getUser(null));
+        Assertions.assertNull(users.getUser(""));
+    }
+
+    @Test
+    void getUserByGithubUsername_success() {
+        EclipseUser u = users.getUserByGithubUsername("grunter");
+        // assert that this is the user we expect and that it exists
+        Assertions.assertTrue(u != null);
+        Assertions.assertTrue(u.getMail().equals("grunt@important.co"));
+    }
+
+    @Test
+    void getUserByGithubUsername_doesNotExist() {
+        Assertions.assertNull(users.getUserByGithubUsername("nota.realboy"));
+    }
+
+    @Test
+    void getUserByGithubUsername_nullOrEmptyUName() {
+        Assertions.assertNull(users.getUserByGithubUsername(null));
+        Assertions.assertNull(users.getUserByGithubUsername(""));
+    }
+
+    @Test
+    void isUserABot_success() {
+        Assertions.assertTrue(users.userIsABot("1.bot@eclipse.org", getTestProject()));
+    }
+
+    @Test
+    void isUserABot_nullOrEmptyAddress() {
+        Assertions.assertFalse(users.userIsABot(null, getTestProject()));
+        Assertions.assertFalse(users.userIsABot("", getTestProject()));
+    }
+
+    @Test
+    void isUserABot_nullOrEmptyProjects_botAddress() {
+        Assertions.assertTrue(users.userIsABot("2.bot@eclipse.org", null));
+        Assertions.assertTrue(users.userIsABot("2.bot@eclipse.org", Collections.emptyList()));
+    }
+
+    @Test
+    void isUserABot_nullOrEmptyProjects_otherAddress() {
+        Assertions.assertFalse(users.userIsABot("grunt@important.co", null));
+        Assertions.assertFalse(users.userIsABot("grunt@important.co", Collections.emptyList()));
+    }
+
+    @Test
+    void isUserABot_doesNotMatchOtherProjects() {
+        Assertions.assertFalse(users.userIsABot("2.bot@eclipse.org", getTestProject()));
+    }
+
+    @Test
+    void isUserABot_noMatch() {
+        Assertions.assertFalse(users.userIsABot("grunt@important.co", getTestProject()));
+    }
+
+    /**
+     * Gets the sample.proj project to test for project connections when needed.
+     * 
+     * @return the sample.proj project in a list for use in tests.
+     */
+    private List<Project> getTestProject() {
+        Optional<Project> proj = projects.getProjects().stream().filter(p -> p.getProjectId().equals("sample.proj"))
+                .findFirst();
+        if (proj.isEmpty()) {
+            Assertions.fail("Could not find one of the needed test projects for test, bad state.");
+        }
+        return Arrays.asList(proj.get());
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationServiceTest.java
new file mode 100644
index 00000000..f0da1219
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationServiceTest.java
@@ -0,0 +1,176 @@
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.core.model.FlatRequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.GitUser;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.namespace.ProviderType;
+import org.eclipsefoundation.git.eca.service.ValidationService;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+class DefaultValidationServiceTest {
+
+    @Inject
+    ValidationService validation;
+
+    @Test
+    void generateRequestHash_reproducible() {
+        ValidationRequest vr = generateBaseRequest();
+        String fingerprint = validation.generateRequestHash(vr);
+        // if pushing the same set of commits without change the fingerprint won't change
+        Assertions.assertEquals(fingerprint, validation.generateRequestHash(vr));
+    }
+
+    @Test
+    void generateRequestHash_changesWithRepoURL() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        // generate initial fingerprint
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        String fingerprint = validation.generateRequestHash(vr);
+        // generate request with different repo url
+        vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/other-sample")).setCommits(commits).build();
+        // fingerprint should change based on repo URL to reduce risk of collision
+        Assertions.assertNotEquals(fingerprint, validation.generateRequestHash(vr));
+    }
+
+    @Test
+    void generateRequestHash_changesWithNewCommits() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        // generate initial fingerprint
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        String fingerprint = validation.generateRequestHash(vr);
+        Commit c2 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c2);
+        // generate request with different repo url
+        vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/other-sample")).setCommits(commits).build();
+        // each commit added should modify the fingerprint at least slightly
+        Assertions.assertNotEquals(fingerprint, validation.generateRequestHash(vr));
+    }
+
+    @Test
+    void getHistoricValidationStatus_noFingerprint() {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        Assertions.assertTrue(validation.getHistoricValidationStatus(wrap, null).isEmpty());
+        Assertions.assertTrue(validation.getHistoricValidationStatus(wrap, " ").isEmpty());
+        Assertions.assertTrue(validation.getHistoricValidationStatus(wrap, "").isEmpty());
+    }
+
+    @Test
+    void getHistoricValidationStatus_noResultsForFingerprint() {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        Assertions.assertTrue(validation.getHistoricValidationStatus(wrap, UUID.randomUUID().toString()).isEmpty());
+    }
+
+    @Test
+    void getHistoricValidationStatus_success() {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        Assertions.assertTrue(!validation.getHistoricValidationStatus(wrap, "957706b0f31e0ccfc5287c0ebc62dc79").isEmpty());
+    }
+
+    @Test
+    void getRequestCommitValidationStatus_noneExisting() {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        ValidationRequest vr = generateBaseRequest();
+        List<CommitValidationStatus> out = validation.getRequestCommitValidationStatus(wrap, vr, "sample.proj");
+        // should always return non-null, should be empty w/ no results as there shouldn't be a matching status
+        Assertions.assertTrue(out.isEmpty());
+    }
+
+    @Test
+    void getRequestCommitValidationStatus_existing() {
+        // create request that lines up with one of the existing test commit validation statuses
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        List<CommitValidationStatus> out = validation.getRequestCommitValidationStatus(wrap, vr, "sample.proj");
+        // should contain one of the test status objects
+        Assertions.assertTrue(!out.isEmpty());
+    }
+
+    @Test
+    void getRequestCommitValidationStatus_noProjectWithResults() {
+        // create request that lines up with one of the existing test commit validation statuses
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("abc123def456")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        List<CommitValidationStatus> out = validation.getRequestCommitValidationStatus(wrap, vr, null);
+        // should contain one of the test status objects
+        Assertions.assertTrue(!out.isEmpty());
+    }
+
+    @Test
+    void getRequestCommitValidationStatus_noProjectWithNoResults() {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("http://localhost/git/eca"));
+        ValidationRequest vr = generateBaseRequest();
+        List<CommitValidationStatus> out = validation.getRequestCommitValidationStatus(wrap, vr, null);
+        // should contain one of the test status objects
+        Assertions.assertTrue(out.isEmpty());
+    }
+
+    /**
+     * Used when a random validationRequest is needed and will not need to be recreated/modified. Base request should
+     * register as a commit for the test `sample.proj` project.
+     * 
+     * @return random basic validation request.
+     */
+    private ValidationRequest generateBaseRequest() {
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash(UUID.randomUUID().toString())
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+        return ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
new file mode 100644
index 00000000..b7a62e24
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
@@ -0,0 +1,69 @@
+package org.eclipsefoundation.git.eca.test.api;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.git.eca.api.AccountsAPI;
+import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.model.EclipseUser.ECA;
+
+import io.quarkus.test.Mock;
+
+/**
+ * Simple stub for accounts API. Allows for easy testing of users that don't really exist upstream, and so that we don't
+ * need a real auth token for data.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Mock
+@RestClient
+@ApplicationScoped
+public class MockAccountsAPI implements AccountsAPI {
+
+    private Map<String, EclipseUser> users;
+
+    public MockAccountsAPI() {
+        int id = 0;
+        this.users = new HashMap<>();
+        users.put("newbie@important.co", EclipseUser.builder().setIsCommitter(false).setUid(id++)
+                .setMail("newbie@important.co").setName("newbieAnon").setECA(ECA.builder().build()).build());
+        users.put("slom@eclipse-foundation.org",
+                EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("slom@eclipse-foundation.org")
+                        .setName("barshall_blathers")
+                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
+        users.put("tester@eclipse-foundation.org",
+                EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("tester@eclipse-foundation.org")
+                        .setName("mctesterson")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        users.put("code.wiz@important.co",
+                EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("code.wiz@important.co")
+                        .setName("da_wizz")
+                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
+        users.put("grunt@important.co",
+                EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co").setName("grunter")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        users.put("paper.pusher@important.co",
+                EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("paper.pusher@important.co")
+                        .setName("sumAnalyst")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+
+    }
+
+    @Override
+    public List<EclipseUser> getUsers(String token, String mail) {
+        return Arrays.asList(users.get(mail));
+    }
+
+    @Override
+    public EclipseUser getUserByGithubUname(String token, String uname) {
+        // assumes github id is same as uname for purposes of lookup (simplifies fetch logic)
+        return users.values().stream().filter(u -> u.getName().equals(uname)).findFirst().orElseGet(() -> null);
+    }
+
+}
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 1614a2e5..2ada62dd 100644
--- a/src/test/resources/database/default/V1.0.0__default.sql
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -10,9 +10,10 @@ CREATE TABLE CommitValidationStatus (
   repoUrl varchar(100) NOT NULL,
   PRIMARY KEY (id)
 );
-INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(1,'123456789', 'sample.proj', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(1,'123456789', 'sample.proj', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
 INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(2,'123456789', 'sample.proto', NOW(), NOW(),'GITLAB','');
 INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(3,'987654321', 'sample.proto', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,sha,lastModified,creationDate,provider, repoUrl) VALUES(5,'abc123def456', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
 
 CREATE TABLE CommitValidationMessage (
   providerId varchar(100) DEFAULT NULL,
@@ -24,4 +25,11 @@ CREATE TABLE CommitValidationMessage (
   PRIMARY KEY (id)
 );
 
-INSERT INTO  CommitValidationMessage(id,commit_id,providerId,authorEmail,eclipseId,statusCode) VALUES(4,1,'','','',-405);
+INSERT INTO CommitValidationMessage(id,commit_id,providerId,authorEmail,eclipseId,statusCode) VALUES(4,1,'','','',-405);
+
+CREATE TABLE `CommitValidationStatusGrouping` (
+  `fingerprint` varchar(255) NOT NULL,
+  `commit_id` int(11) NOT NULL,
+  PRIMARY KEY (`commit_id`,`fingerprint`)
+);
+INSERT INTO CommitValidationStatusGrouping(fingerprint, commit_id) VALUES('957706b0f31e0ccfc5287c0ebc62dc79', 1);
\ No newline at end of file
-- 
GitLab


From c4f1eacddcab06768c7c6a4185013a97c4149cf0 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 25 May 2022 15:15:12 -0400
Subject: [PATCH 08/10] Update openapi spec for ECA response, and added commit
 status endpoint

Additionally adds missing fingerprint variable to the response object
for eca validation requests.
---
 spec/openapi.yaml                             | 77 ++++++++++++++++++-
 .../git/eca/model/ValidationResponse.java     |  4 +
 .../git/eca/resource/ValidationResource.java  |  3 +-
 3 files changed, 82 insertions(+), 2 deletions(-)

diff --git a/spec/openapi.yaml b/spec/openapi.yaml
index 6fb0172c..0e30da70 100644
--- a/spec/openapi.yaml
+++ b/spec/openapi.yaml
@@ -32,6 +32,21 @@ paths:
                         $ref: '#/components/schemas/ValidationResponse'
             500:
                description: Error while retrieving data
+   /eca/{fingerprint}:
+      get:
+        tags:
+        - ECA Validation Status
+        summary: Historic ECA validation status
+        description: Returns a set of validation messages for the given unique fingerprint
+        responses:
+          200:
+            description: Success
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/CommitValidationStatuses'
+          404:
+            description: Could not find any commits for given fingerprint
 components:
    schemas:
       NullableString:
@@ -61,7 +76,7 @@ components:
             description: the outward facing URL of the repo the commit belongs to.
           strictMode:
             type: boolean
-            description: asd
+            description: Whether to strictly apply validation regardless of project matching
           commits:
             type: array
             minimum: 1
@@ -114,6 +129,9 @@ components:
             strictMode:
                type: boolean
                description: Whether strict mode was enforced for the validation.
+            fingerprint:
+               type: string
+               description: The unique fingerprint to use when looking up commit data
             errorCount:
                type: integer
                description: The number of errors encountered while validating the request
@@ -153,3 +171,60 @@ components:
           message:
             type: string
             description: Information about the commit message. This can either be information about the validation process to report or the source of an error to be corrected.
+      CommitValidationStatuses:
+        type: array
+        items:
+          $ref: '#/components/schemas/CommitValidationStatus'
+      CommitValidationStatus:
+        type: object
+        properties:
+          id:
+            type: integer
+            description: internal ID of the commit for tracking
+          sha:
+            type: string
+            description: the SHA of the commit that was validated
+          project:
+            type: string
+            description: The short project ID of the project that was detected for the commit, if it exists.
+          repo_url:
+            type: string
+            description: the outward facing URL of the repo the commit belongs to.
+          provider:
+            type: string
+            description: The provider for which the commit is being validated for
+            enum:
+              - github
+              - gitlab
+              - gerrit
+          creation_date:
+               $ref: '#/components/schemas/DateTime'
+               description: Time that the commit was first attempted to be validated
+          last_modified:
+               $ref: '#/components/schemas/DateTime'
+               description: The latest tracked time of validation.
+          errors:
+            type: array
+            items:
+              $ref: '#/components/schemas/CommitValidationMessage'
+      CommitValidationMessage:
+        type: object
+        properties:
+          id:
+            type: integer
+            description: internal ID of the commit message for tracking
+          commit_id:
+            type: integer
+            description: internal ID of the commit for tracking
+          status_code:
+            type: integer
+            description: error code associated with the commit 
+          eclipse_id:
+            type: string
+            description: the Eclipse Foundation ID of the user 
+          author_email:
+            type: string
+            description: Email address of the author of the commit for additional information
+          provider_id:
+            type: string
+            description: the outward facing URL of the repo the commit belongs to.
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 f6c0b0f3..ec16eb94 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
@@ -44,6 +44,8 @@ public abstract class ValidationResponse {
     public abstract boolean getTrackedProject();
 
     public abstract boolean getStrictMode();
+    
+    public abstract String getFingerprint();
 
     public boolean getPassed() {
         return getErrorCount() <= 0;
@@ -103,6 +105,8 @@ public abstract class ValidationResponse {
         public abstract Builder setTrackedProject(boolean trackedProject);
 
         public abstract Builder setStrictMode(boolean strictMode);
+        
+        public abstract Builder setFingerprint(String fingerprint);
 
         public abstract ValidationResponse build();
     }
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 5e83555f..5b5dd302 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -107,7 +107,8 @@ public class ValidationResource {
             List<Project> filteredProjects = retrieveProjectsForRequest(req);
             ValidationResponse r = ValidationResponse.builder()
                     .setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
-                    .setTrackedProject(!filteredProjects.isEmpty()).build();
+                    .setTrackedProject(!filteredProjects.isEmpty()).setFingerprint(validation.generateRequestHash(req))
+                    .build();
             List<CommitValidationStatus> statuses = validation.getRequestCommitValidationStatus(wrapper, req,
                     filteredProjects.isEmpty() ? null : filteredProjects.get(0).getProjectId());
             for (Commit c : req.getCommits()) {
-- 
GitLab


From c68b8a8a8d459c2bf5f7e047758ff1f89a454971 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 1 Jun 2022 10:55:52 -0400
Subject: [PATCH 09/10] Update SHA field to be commitHash, update SQL + filters
 accordingly

---
 config/mariadb/init.sql                       | 16 +++++++---
 .../git/eca/dto/CommitValidationMessage.java  |  8 ++---
 .../git/eca/dto/CommitValidationStatus.java   | 32 +++++++++----------
 .../dto/CommitValidationStatusGrouping.java   |  2 +-
 .../git/eca/resource/ValidationResource.java  |  2 +-
 .../impl/DefaultValidationService.java        |  4 +--
 .../templates/simple_fingerprint_ui.html      |  6 ++--
 7 files changed, 38 insertions(+), 32 deletions(-)

diff --git a/config/mariadb/init.sql b/config/mariadb/init.sql
index 30d455ed..31ad11e3 100644
--- a/config/mariadb/init.sql
+++ b/config/mariadb/init.sql
@@ -1,6 +1,6 @@
 CREATE TABLE `CommitValidationMessage` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `commit_id` int(11) NOT NULL,
+  `id` SERIAL,
+  `commit_id` bigint(20) NOT NULL,
   `providerId` varchar(100) DEFAULT NULL,
   `authorEmail` varchar(100) DEFAULT NULL,
   `eclipseId` varchar(255) DEFAULT NULL,
@@ -9,12 +9,18 @@ CREATE TABLE `CommitValidationMessage` (
 );
 
 CREATE TABLE `CommitValidationStatus` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `sha` varchar(100) NOT NULL,
+  `id` SERIAL,
+  `commitHash` varchar(100) NOT NULL,
   `project` varchar(100) NOT NULL,
   `lastModified` datetime DEFAULT NULL,
   `creationDate` datetime DEFAULT NULL,
   `provider` varchar(100) NOT NULL,
-  `repoUrl` varchar(100) DEFAULT NULL,
+  `repoUrl` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `CommitValidationStatusGrouping` (
+  `fingerprint` varchar(255) NOT NULL,
+  `commit_id` bigint(20)  NOT NULL,
+  PRIMARY KEY (`commit_id`,`fingerprint`)
 );
\ No newline at end of file
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 e0daa0a0..b9cde187 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java
@@ -28,7 +28,7 @@ public class CommitValidationMessage extends BareNode {
 
     @Id
     @GeneratedValue(strategy=GenerationType.IDENTITY)
-    private int id;
+    private Long id;
     @ManyToOne
     private CommitValidationStatus commit;
     private int statusCode;
@@ -37,14 +37,14 @@ public class CommitValidationMessage extends BareNode {
     private String providerId;
 
     @Override
-    public Object getId() {
+    public Long getId() {
         return id;
     }
 
     /**
      * @param id the id to set
      */
-    public void setId(int id) {
+    public void setId(Long id) {
         this.id = id;
     }
 
@@ -133,7 +133,7 @@ public class CommitValidationMessage extends BareNode {
                 String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
                 if (StringUtils.isNumeric(id)) {
                     stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?",
-                            new Object[] { Integer.valueOf(id) }));
+                            new Object[] { Long.valueOf(id) }));
                 }
                 // commit id check
                 String commitId = params.getFirst(GitEcaParameterNames.COMMIT_ID.getName());
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
index 9fb1c844..b9271f5b 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
@@ -31,8 +31,8 @@ public class CommitValidationStatus extends BareNode {
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private int id;
-    private String sha;
+    private Long id;
+    private String commitHash;
     private String project;
     private String repoUrl;
     @Enumerated(EnumType.STRING)
@@ -43,26 +43,26 @@ public class CommitValidationStatus extends BareNode {
     private List<CommitValidationMessage> errors;
 
     @Override
-    public Integer getId() {
+    public Long getId() {
         return id;
     }
 
-    public void setId(int id) {
+    public void setId(Long id) {
         this.id = id;
     }
 
     /**
      * @return the sha
      */
-    public String getSha() {
-        return sha;
+    public String getCommitHash() {
+        return commitHash;
     }
 
     /**
-     * @param sha the sha to set
+     * @param commitHash the sha to set
      */
-    public void setSha(String sha) {
-        this.sha = sha;
+    public void setCommitHash(String commitHash) {
+        this.commitHash = commitHash;
     }
 
     /**
@@ -155,7 +155,7 @@ public class CommitValidationStatus extends BareNode {
         builder.append("CommitValidationStatus [id=");
         builder.append(id);
         builder.append(", sha=");
-        builder.append(sha);
+        builder.append(commitHash);
         builder.append(", project=");
         builder.append(project);
         builder.append(", repoUrl=");
@@ -181,20 +181,20 @@ public class CommitValidationStatus extends BareNode {
         public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
             ParameterizedSQLStatement stmt = builder.build(TABLE);
             // sha check
-            String sha = params.getFirst(GitEcaParameterNames.SHA.getName());
-            if (StringUtils.isNumeric(sha)) {
+            String commitHash = params.getFirst(GitEcaParameterNames.SHA.getName());
+            if (StringUtils.isNumeric(commitHash)) {
                 stmt.addClause(
-                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".sha = ?", new Object[] { sha }));
+                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash = ?", new Object[] { commitHash }));
             }
             String projectId = params.getFirst(GitEcaParameterNames.PROJECT_ID.getName());
             if (StringUtils.isNumeric(projectId)) {
                 stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?",
                         new Object[] { projectId }));
             }
-            List<String> shas = params.get(GitEcaParameterNames.SHAS.getName());
-            if (shas != null && !shas.isEmpty()) {
+            List<String> commitHashes = params.get(GitEcaParameterNames.SHAS.getName());
+            if (commitHashes != null && !commitHashes.isEmpty()) {
                 stmt.addClause(
-                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".sha IN ?", new Object[] { shas }));
+                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash IN ?", new Object[] { commitHashes }));
             }
             String repoUrl = params.getFirst(GitEcaParameterNames.REPO_URL.getName());
             if (StringUtils.isNotBlank(repoUrl)) {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
index 4441c2fb..2b562ea4 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatusGrouping.java
@@ -38,7 +38,7 @@ public class CommitValidationStatusGrouping extends BareNode {
 
     @Override
     public GroupingCompositeId getId() {
-        return compositeId;
+        return getCompositeId();
     }
 
     /**
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 5b5dd302..9bc1c102 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -113,7 +113,7 @@ public class ValidationResource {
                     filteredProjects.isEmpty() ? null : filteredProjects.get(0).getProjectId());
             for (Commit c : req.getCommits()) {
                 // get the current status if present
-                Optional<CommitValidationStatus> status = statuses.stream().filter(s -> c.getHash().equals(s.getSha()))
+                Optional<CommitValidationStatus> status = statuses.stream().filter(s -> c.getHash().equals(s.getCommitHash()))
                         .findFirst();
                 // skip the commit validation if already passed
                 if (status.isPresent() && status.get().getErrors().isEmpty()) {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
index b264525e..cf117411 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -81,7 +81,7 @@ public class DefaultValidationService implements ValidationService {
                 continue;
             }
             // update the status if present, otherwise make new one.
-            Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getSha()))
+            Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getCommitHash()))
                     .findFirst();
             CommitValidationStatus base;
             if (status.isPresent()) {
@@ -89,7 +89,7 @@ public class DefaultValidationService implements ValidationService {
             } else {
                 base = new CommitValidationStatus();
                 base.setProject(p != null ? p.getProjectId() : null);
-                base.setSha(e.getKey());
+                base.setCommitHash(e.getKey());
                 base.setProvider(req.getProvider());
                 base.setRepoUrl(req.getRepoUrl().toString());
                 base.setCreationDate(DateTimeHelper.now());
diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html
index dc3851c4..bea82497 100644
--- a/src/main/resources/templates/simple_fingerprint_ui.html
+++ b/src/main/resources/templates/simple_fingerprint_ui.html
@@ -15,14 +15,14 @@
   <div id="accordion">
     <div class="panel list-group">
       {#for status in statuses.orEmpty}
-      <a href="#{status.sha}" data-parent="#accordion" data-toggle="collapse" class="list-group-item">
+      <a href="#{status.commitHash}" data-parent="#accordion" data-toggle="collapse" class="list-group-item">
       {#if status.errors.size > 0}
       <span class="badge">{status.errors.size}</span>
       {/if}
-        <p>SHA: <strong>{status.sha}</strong></p>
+        <p>SHA: <strong>{status.commitHash}</strong></p>
       </a>
       
-      <div class="padding-30 collapse" id="{status.sha}">
+      <div class="padding-30 collapse" id="{status.commitHash}">
         <div>Last validated: <em>{status.lastModified.format('d MMM uuuu')}</em></div>
         <div>Errors: <em>{status.errors.size}</em></div>
         <ul class="list-group-item-text">
-- 
GitLab


From ceeed191f3a3c41e814f1d97fc31a8b3b72b8391 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 1 Jun 2022 11:11:26 -0400
Subject: [PATCH 10/10] Fix test db schema for change from sha => commitHash

---
 .../database/default/V1.0.0__default.sql      | 20 +++++++++----------
 1 file changed, 9 insertions(+), 11 deletions(-)

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 2ada62dd..5caeee24 100644
--- a/src/test/resources/database/default/V1.0.0__default.sql
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -1,25 +1,23 @@
-
-
 CREATE TABLE CommitValidationStatus (
-  sha varchar(100) NOT NULL,
+  commitHash varchar(100) NOT NULL,
   project varchar(100) DEFAULT NULL,
   lastModified datetime DEFAULT NULL,
   creationDate datetime DEFAULT NULL,
-  id int(11) NOT NULL AUTO_INCREMENT,
+  id bigint(20) NOT NULL AUTO_INCREMENT,
   provider varchar(100) NOT NULL,
-  repoUrl varchar(100) NOT NULL,
+  repoUrl varchar(255) NOT NULL,
   PRIMARY KEY (id)
 );
-INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(1,'123456789', 'sample.proj', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
-INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(2,'123456789', 'sample.proto', NOW(), NOW(),'GITLAB','');
-INSERT INTO CommitValidationStatus(id,sha,project,lastModified,creationDate,provider, repoUrl) VALUES(3,'987654321', 'sample.proto', NOW(), NOW(),'GITLAB','');
-INSERT INTO CommitValidationStatus(id,sha,lastModified,creationDate,provider, repoUrl) VALUES(5,'abc123def456', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
+INSERT INTO CommitValidationStatus(id,commitHash,project,lastModified,creationDate,provider, repoUrl) VALUES(1,'123456789', 'sample.proj', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
+INSERT INTO CommitValidationStatus(id,commitHash,project,lastModified,creationDate,provider, repoUrl) VALUES(2,'123456789', 'sample.proto', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,commitHash,project,lastModified,creationDate,provider, repoUrl) VALUES(3,'987654321', 'sample.proto', NOW(), NOW(),'GITLAB','');
+INSERT INTO CommitValidationStatus(id,commitHash,lastModified,creationDate,provider, repoUrl) VALUES(5,'abc123def456', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');
 
 CREATE TABLE CommitValidationMessage (
   providerId varchar(100) DEFAULT NULL,
   authorEmail varchar(100) DEFAULT NULL,
   eclipseId varchar(255) DEFAULT NULL,
-  id int(11) NOT NULL AUTO_INCREMENT,
+  id bigint(20) NOT NULL AUTO_INCREMENT,
   commit_id int(11) NOT NULL,
   statusCode int(11) NOT NULL,
   PRIMARY KEY (id)
@@ -29,7 +27,7 @@ INSERT INTO CommitValidationMessage(id,commit_id,providerId,authorEmail,eclipseI
 
 CREATE TABLE `CommitValidationStatusGrouping` (
   `fingerprint` varchar(255) NOT NULL,
-  `commit_id` int(11) NOT NULL,
+  `commit_id` bigint(20) NOT NULL,
   PRIMARY KEY (`commit_id`,`fingerprint`)
 );
 INSERT INTO CommitValidationStatusGrouping(fingerprint, commit_id) VALUES('957706b0f31e0ccfc5287c0ebc62dc79', 1);
\ No newline at end of file
-- 
GitLab