add jacoco test code coverage
This commit is contained in:
parent
86802a2aab
commit
a66ed8e59f
23
README.md
23
README.md
@ -369,6 +369,29 @@ To apply formatting rules, use:
|
|||||||
gw spotlessApply
|
gw spotlessApply
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JaCoCo Test Code Coverage Check
|
||||||
|
|
||||||
|
This project uses the JaCoCo test code coverage report with limit checks.
|
||||||
|
It can be executed with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw jacocoTestReport
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also automatically run after `gw test`.
|
||||||
|
It is configured in [build.gradle](build.gradle).
|
||||||
|
|
||||||
|
A report is generated under [build/reports/jacoco/tests/test/index.html](./build/reports/jacoco/test/html/index.html).
|
||||||
|
|
||||||
|
Additionally, quality limits are checked via:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also executed as part of `gw check`.
|
||||||
|
|
||||||
|
|
||||||
### OWASP Security Vulnerability Check
|
### OWASP Security Vulnerability Check
|
||||||
|
|
||||||
An OWASP security vulnerability is configured and can be utilized by running:
|
An OWASP security vulnerability is configured and can be utilized by running:
|
||||||
|
97
build.gradle
97
build.gradle
@ -6,6 +6,7 @@ plugins {
|
|||||||
id 'com.github.jk1.dependency-license-report' version '2.1'
|
id 'com.github.jk1.dependency-license-report' version '2.1'
|
||||||
id "org.owasp.dependencycheck" version "7.1.1"
|
id "org.owasp.dependencycheck" version "7.1.1"
|
||||||
id "com.diffplug.spotless" version "6.9.0"
|
id "com.diffplug.spotless" version "6.9.0"
|
||||||
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'net.hostsharing'
|
group = 'net.hostsharing'
|
||||||
@ -71,14 +72,19 @@ dependencyManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Java Compiler Options
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
options.compilerArgs += ["-parameters"]
|
options.compilerArgs += [
|
||||||
|
"-parameters" // keep parameter names => no need for @Param for SpringData
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use JUnit Jupiter
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenAPI Source Code Generation
|
||||||
openapiProcessor {
|
openapiProcessor {
|
||||||
spring {
|
spring {
|
||||||
processor 'io.openapiprocessor:openapi-processor-spring:2022.4'
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.4'
|
||||||
@ -93,6 +99,7 @@ sourceSets.main.java.srcDir 'build/generated/sources/openapi'
|
|||||||
project.tasks.processResources.dependsOn('processSpring')
|
project.tasks.processResources.dependsOn('processSpring')
|
||||||
project.tasks.compileJava.dependsOn('processSpring')
|
project.tasks.compileJava.dependsOn('processSpring')
|
||||||
|
|
||||||
|
// Spotless Code Formatting
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
// removeUnusedImports() TODO: reactivate once it can deal with multi-line-strings
|
// removeUnusedImports() TODO: reactivate once it can deal with multi-line-strings
|
||||||
@ -108,6 +115,7 @@ spotless {
|
|||||||
}
|
}
|
||||||
project.tasks.check.dependsOn(spotlessCheck)
|
project.tasks.check.dependsOn(spotlessCheck)
|
||||||
|
|
||||||
|
// OWASP Dependency Security Test
|
||||||
dependencyCheck {
|
dependencyCheck {
|
||||||
cveValidForHours=4
|
cveValidForHours=4
|
||||||
format = 'ALL'
|
format = 'ALL'
|
||||||
@ -117,8 +125,95 @@ dependencyCheck {
|
|||||||
}
|
}
|
||||||
project.tasks.check.dependsOn(dependencyCheckAnalyze)
|
project.tasks.check.dependsOn(dependencyCheckAnalyze)
|
||||||
|
|
||||||
|
// License Check
|
||||||
licenseReport {
|
licenseReport {
|
||||||
excludeBoms = true
|
excludeBoms = true
|
||||||
allowedLicensesFile = new File("$projectDir/etc/allowed-licenses.json")
|
allowedLicensesFile = new File("$projectDir/etc/allowed-licenses.json")
|
||||||
}
|
}
|
||||||
project.tasks.check.dependsOn(checkLicense)
|
project.tasks.check.dependsOn(checkLicense)
|
||||||
|
|
||||||
|
// JaCoCo Test Code Coverage
|
||||||
|
jacoco {
|
||||||
|
toolVersion = "0.8.8"
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
finalizedBy jacocoTestReport // generate report after tests
|
||||||
|
excludes = [
|
||||||
|
'net.hostsharing.hsadminng.generated.**',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
jacocoTestReport {
|
||||||
|
dependsOn test
|
||||||
|
afterEvaluate {
|
||||||
|
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||||
|
fileTree(dir: it, exclude: [
|
||||||
|
"net/hostsharing/hsadminng/generated/**/*.class",
|
||||||
|
|
||||||
|
// TODO: improve test code coverage for these classes:
|
||||||
|
"net/hostsharing/hsadminng/rbac/rbacuser/UserController.class",
|
||||||
|
"net/hostsharing/hsadminng/rbac/rbacgrant/GrantController.class",
|
||||||
|
"net/hostsharing/hsadminng/hs/hscustomer/CustomerController.class"
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
|
println "HTML Jacoco Test Code Coverage Report: file://${reports.html.outputLocation.get()}/index.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.tasks.check.dependsOn(jacocoTestCoverageVerification)
|
||||||
|
jacocoTestCoverageVerification {
|
||||||
|
violationRules {
|
||||||
|
rule {
|
||||||
|
excludes = ['net.hostsharing.hsadminng.generated.**']
|
||||||
|
limit {
|
||||||
|
minimum = 0.7 // TODO: increase to 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// element: PACKAGE, BUNDLE, CLASS, SOURCEFILE or METHOD
|
||||||
|
// counter: INSTRUCTION, BRANCH, LINE, COMPLEXITY, METHOD, or CLASS
|
||||||
|
// value: TOTALCOUNT, COVEREDCOUNT, MISSEDCOUNT, COVEREDRATIO or MISSEDRATIO
|
||||||
|
|
||||||
|
rule {
|
||||||
|
element = 'CLASS'
|
||||||
|
excludes = [
|
||||||
|
'net.hostsharing.hsadminng.generated.**',
|
||||||
|
'net.hostsharing.hsadminng.HsadminNgApplication',
|
||||||
|
'net.hostsharing.hsadminng.TestController',
|
||||||
|
|
||||||
|
// TODO: improve test code coverage:
|
||||||
|
'net.hostsharing.hsadminng.rbac.rbacuser.UserController',
|
||||||
|
'net.hostsharing.hsadminng.hs.hscustomer.CustomerController'
|
||||||
|
]
|
||||||
|
|
||||||
|
limit {
|
||||||
|
counter = 'LINE'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
element = 'METHOD'
|
||||||
|
excludes = [
|
||||||
|
'net.hostsharing.hsadminng.generated.**',
|
||||||
|
'net.hostsharing.hsadminng.HsadminNgApplication.*',
|
||||||
|
|
||||||
|
// TODO: improve test code coverage:
|
||||||
|
'net.hostsharing.hsadminng.rbac.rbacuser.RbacUserController.listUsers(*)',
|
||||||
|
'net.hostsharing.hsadminng.rbac.rbacuser.RbacUserController.listUserPermissions(*)',
|
||||||
|
'net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantController.listUserGrants(*)',
|
||||||
|
'net.hostsharing.hsadminng.hs.hscustomer.CustomerController.addCustomer(java.lang.String, java.lang.String, net.hostsharing.hsadminng.generated.api.v1.model.CustomerResource)'
|
||||||
|
]
|
||||||
|
|
||||||
|
limit {
|
||||||
|
counter = 'BRANCH'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.5 // TODO: increase test code coverage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1
lombok.config
Normal file
1
lombok.config
Normal file
@ -0,0 +1 @@
|
|||||||
|
lombok.addLombokGeneratedAnnotation = true
|
@ -31,14 +31,14 @@ public class RestResponseEntityExceptionHandler
|
|||||||
protected ResponseEntity<CustomErrorResponse> handleJpaExceptions(
|
protected ResponseEntity<CustomErrorResponse> handleJpaExceptions(
|
||||||
final RuntimeException exc, final WebRequest request) {
|
final RuntimeException exc, final WebRequest request) {
|
||||||
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
||||||
return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message);
|
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Throwable.class)
|
@ExceptionHandler(Throwable.class)
|
||||||
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
||||||
final RuntimeException exc, final WebRequest request) {
|
final Throwable exc, final WebRequest request) {
|
||||||
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
||||||
return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message);
|
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<HttpStatus> httpStatus(final String message) {
|
private Optional<HttpStatus> httpStatus(final String message) {
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RestResponseEntityExceptionHandlerUnitTest {
|
||||||
|
|
||||||
|
final RestResponseEntityExceptionHandler exceptionHandler = new RestResponseEntityExceptionHandler();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleConflict() {
|
||||||
|
// given
|
||||||
|
final var givenException = new DataIntegrityViolationException("First Line\nSecond Line\nThird Line");
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleConflict(givenException, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(409);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("First Line");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jpaExceptionWithKnownErrorCode() {
|
||||||
|
// given
|
||||||
|
final var givenException = new JpaSystemException(new RuntimeException(
|
||||||
|
"ERROR: [401] First Line\nSecond Line\nThird Line"));
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleJpaExceptions(givenException, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(401);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [401] First Line");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jpaExceptionWithUnknownErrorCode() {
|
||||||
|
// given
|
||||||
|
final var givenException = new JpaSystemException(new RuntimeException(
|
||||||
|
"ERROR: [999] First Line\nSecond Line\nThird Line"));
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleJpaExceptions(givenException, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(500);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [999] First Line");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleOtherExceptionsWithoutErrorCode() {
|
||||||
|
// given
|
||||||
|
final var givenThrowable = new Error("First Line\nSecond Line\nThird Line");
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleOtherExceptions(givenThrowable, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(500);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("First Line");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleOtherExceptionsWithErrorCode() {
|
||||||
|
// given
|
||||||
|
final var givenThrowable = new Error("ERROR: [418] First Line\nSecond Line\nThird Line");
|
||||||
|
final var givenWebRequest = mock(WebRequest.class);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var errorResponse = exceptionHandler.handleOtherExceptions(givenThrowable, givenWebRequest);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(errorResponse.getStatusCodeValue()).isEqualTo(418);
|
||||||
|
assertThat(errorResponse.getBody().getMessage()).isEqualTo("ERROR: [418] First Line");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hscustomer;
|
package net.hostsharing.hsadminng.hs.hscustomer;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
@ -28,6 +29,9 @@ class CustomerControllerRestTest {
|
|||||||
@MockBean
|
@MockBean
|
||||||
CustomerRepository customerRepositoryMock;
|
CustomerRepository customerRepositoryMock;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ListCustomers {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listCustomersWillReturnAllCustomersFromRepositoryIfNoCriteriaGiven() throws Exception {
|
void listCustomersWillReturnAllCustomersFromRepositoryIfNoCriteriaGiven() throws Exception {
|
||||||
|
|
||||||
@ -101,5 +105,6 @@ class CustomerControllerRestTest {
|
|||||||
verify(contextMock).setCurrentUser("mike@hostsharing.net");
|
verify(contextMock).setCurrentUser("mike@hostsharing.net");
|
||||||
verify(contextMock).assumeRoles("admin@yyy.example.com");
|
verify(contextMock).assumeRoles("admin@yyy.example.com");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import org.springframework.transaction.annotation.Propagation;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.PersistenceException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -125,7 +124,8 @@ class RbacGrantRepositoryIntegrationTest {
|
|||||||
assertThat(attempt.caughtException()).isNull();
|
assertThat(attempt.caughtException()).isNull();
|
||||||
assertThat(rbacGrantRepository.findAll())
|
assertThat(rbacGrantRepository.findAll())
|
||||||
.extracting(RbacGrantEntity::toDisplay)
|
.extracting(RbacGrantEntity::toDisplay)
|
||||||
.contains("{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
|
.contains(
|
||||||
|
"{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -238,8 +238,8 @@ class RbacGrantRepositoryIntegrationTest {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
revokeAttempt.assertExceptionWithRootCauseMessage(
|
revokeAttempt.assertExceptionWithRootCauseMessage(
|
||||||
PersistenceException.class,
|
JpaSystemException.class,
|
||||||
"ERROR: [403] Revoking role created by %s is forbidden for {package#aaa00.admin}." .formatted(
|
"ERROR: [403] Revoking role created by %s is forbidden for {package#aaa00.admin}.".formatted(
|
||||||
grantedByRole.getUuid()
|
grantedByRole.getUuid()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -261,7 +261,7 @@ class RbacGrantRepositoryIntegrationTest {
|
|||||||
assumeThat(grantAttempt.caughtException()).isNull();
|
assumeThat(grantAttempt.caughtException()).isNull();
|
||||||
assumeThat(rbacGrantRepository.findAll())
|
assumeThat(rbacGrantRepository.findAll())
|
||||||
.extracting(RbacGrantEntity::toDisplay)
|
.extracting(RbacGrantEntity::toDisplay)
|
||||||
.contains("{ grant assumed role %s to user %s by role %s }" .formatted(
|
.contains("{ grant assumed role %s to user %s by role %s }".formatted(
|
||||||
with.grantedRole, with.granteeUserName, with.assumedRole
|
with.grantedRole, with.granteeUserName, with.assumedRole
|
||||||
));
|
));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user