add jacoco test code coverage

This commit is contained in:
Michael Hoennig 2022-08-19 10:11:19 +02:00
parent 86802a2aab
commit a66ed8e59f
7 changed files with 334 additions and 120 deletions

View File

@ -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:

View File

@ -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
View File

@ -0,0 +1 @@
lombok.addLombokGeneratedAnnotation = true

View File

@ -17,11 +17,11 @@ import java.util.Optional;
@ControllerAdvice @ControllerAdvice
public class RestResponseEntityExceptionHandler public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler { extends ResponseEntityExceptionHandler {
@ExceptionHandler(DataIntegrityViolationException.class) @ExceptionHandler(DataIntegrityViolationException.class)
protected ResponseEntity<CustomErrorResponse> handleConflict( protected ResponseEntity<CustomErrorResponse> handleConflict(
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.CONFLICT, message); return errorResponse(request, HttpStatus.CONFLICT, message);
@ -29,16 +29,16 @@ public class RestResponseEntityExceptionHandler
@ExceptionHandler(JpaSystemException.class) @ExceptionHandler(JpaSystemException.class)
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) {
@ -54,11 +54,11 @@ public class RestResponseEntityExceptionHandler
} }
private static ResponseEntity<CustomErrorResponse> errorResponse( private static ResponseEntity<CustomErrorResponse> errorResponse(
final WebRequest request, final WebRequest request,
final HttpStatus httpStatus, final HttpStatus httpStatus,
final String message) { final String message) {
return new ResponseEntity<>( return new ResponseEntity<>(
new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus); new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus);
} }
private String firstLine(final String message) { private String firstLine(final String message) {

View File

@ -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");
}
}

View File

@ -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,78 +29,82 @@ class CustomerControllerRestTest {
@MockBean @MockBean
CustomerRepository customerRepositoryMock; CustomerRepository customerRepositoryMock;
@Test @Nested
void listCustomersWillReturnAllCustomersFromRepositoryIfNoCriteriaGiven() throws Exception { class ListCustomers {
// given @Test
when(customerRepositoryMock.findCustomerByOptionalPrefixLike(null)).thenReturn(List.of( void listCustomersWillReturnAllCustomersFromRepositoryIfNoCriteriaGiven() throws Exception {
TestCustomer.xxx,
TestCustomer.yyy));
// when // given
mockMvc.perform(MockMvcRequestBuilders when(customerRepositoryMock.findCustomerByOptionalPrefixLike(null)).thenReturn(List.of(
.get("/api/customers") TestCustomer.xxx,
.header("current-user", "mike@hostsharing.net") TestCustomer.yyy));
.accept(MediaType.APPLICATION_JSON))
// when
mockMvc.perform(MockMvcRequestBuilders
.get("/api/customers")
.header("current-user", "mike@hostsharing.net")
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].prefix", is(TestCustomer.xxx.getPrefix())))
.andExpect(jsonPath("$[1].reference", is(TestCustomer.yyy.getReference()))
);
// then // then
.andExpect(status().isOk()) verify(contextMock).setCurrentUser("mike@hostsharing.net");
.andExpect(jsonPath("$", hasSize(2))) verify(contextMock, never()).assumeRoles(anyString());
.andExpect(jsonPath("$[0].prefix", is(TestCustomer.xxx.getPrefix()))) }
.andExpect(jsonPath("$[1].reference", is(TestCustomer.yyy.getReference()))
);
// then @Test
verify(contextMock).setCurrentUser("mike@hostsharing.net"); void listCustomersWillReturnMatchingCustomersFromRepositoryIfCriteriaGiven() throws Exception {
verify(contextMock, never()).assumeRoles(anyString());
}
@Test // given
void listCustomersWillReturnMatchingCustomersFromRepositoryIfCriteriaGiven() throws Exception { when(customerRepositoryMock.findCustomerByOptionalPrefixLike("x")).thenReturn(List.of(TestCustomer.xxx));
// given // when
when(customerRepositoryMock.findCustomerByOptionalPrefixLike("x")).thenReturn(List.of(TestCustomer.xxx)); mockMvc.perform(MockMvcRequestBuilders
.get("/api/customers")
.header("current-user", "mike@hostsharing.net")
.param("prefix", "x")
.accept(MediaType.APPLICATION_JSON))
// when // then
mockMvc.perform(MockMvcRequestBuilders .andExpect(status().isOk())
.get("/api/customers") .andExpect(jsonPath("$", hasSize(1)))
.header("current-user", "mike@hostsharing.net") .andExpect(jsonPath("$[0].prefix", is(TestCustomer.xxx.getPrefix()))
.param("prefix", "x") );
.accept(MediaType.APPLICATION_JSON))
// then // then
.andExpect(status().isOk()) verify(contextMock).setCurrentUser("mike@hostsharing.net");
.andExpect(jsonPath("$", hasSize(1))) verify(contextMock, never()).assumeRoles(anyString());
.andExpect(jsonPath("$[0].prefix", is(TestCustomer.xxx.getPrefix())) }
);
// then @Test
verify(contextMock).setCurrentUser("mike@hostsharing.net"); void listCustomersWillReturnAllCustomersForGivenAssumedRoles() throws Exception {
verify(contextMock, never()).assumeRoles(anyString());
}
@Test // given
void listCustomersWillReturnAllCustomersForGivenAssumedRoles() throws Exception { when(customerRepositoryMock.findCustomerByOptionalPrefixLike(null)).thenReturn(List.of(TestCustomer.yyy));
// given // when
when(customerRepositoryMock.findCustomerByOptionalPrefixLike(null)).thenReturn(List.of(TestCustomer.yyy)); mockMvc.perform(MockMvcRequestBuilders
.get("/api/customers")
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "admin@yyy.example.com")
.accept(MediaType.APPLICATION_JSON))
// when // then
mockMvc.perform(MockMvcRequestBuilders .andExpect(status().isOk())
.get("/api/customers") .andExpect(jsonPath("$", hasSize(1)))
.header("current-user", "mike@hostsharing.net") .andExpect(jsonPath("$[0].prefix", is(TestCustomer.yyy.getPrefix()))
.header("assumed-roles", "admin@yyy.example.com") );
.accept(MediaType.APPLICATION_JSON))
// then // then
.andExpect(status().isOk()) verify(contextMock).setCurrentUser("mike@hostsharing.net");
.andExpect(jsonPath("$", hasSize(1))) verify(contextMock).assumeRoles("admin@yyy.example.com");
.andExpect(jsonPath("$[0].prefix", is(TestCustomer.yyy.getPrefix())) }
);
// then
verify(contextMock).setCurrentUser("mike@hostsharing.net");
verify(contextMock).assumeRoles("admin@yyy.example.com");
} }
} }

View File

@ -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;
@ -62,8 +61,8 @@ class RbacGrantRepositoryIntegrationTest {
// then // then
exactlyTheseRbacGrantsAreReturned( exactlyTheseRbacGrantsAreReturned(
result, result,
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }"); "{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
} }
@Test @Test
@ -77,11 +76,11 @@ class RbacGrantRepositoryIntegrationTest {
// then // then
exactlyTheseRbacGrantsAreReturned( exactlyTheseRbacGrantsAreReturned(
result, result,
"{ grant assumed role customer#aaa.admin to user admin@aaa.example.com by role global#hostsharing.admin }", "{ grant assumed role customer#aaa.admin to user admin@aaa.example.com by role global#hostsharing.admin }",
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }", "{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa01.admin to user aaa01@aaa.example.com by role customer#aaa.admin }", "{ grant assumed role package#aaa01.admin to user aaa01@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa02.admin to user aaa02@aaa.example.com by role customer#aaa.admin }"); "{ grant assumed role package#aaa02.admin to user aaa02@aaa.example.com by role customer#aaa.admin }");
} }
@Test @Test
@ -96,8 +95,8 @@ class RbacGrantRepositoryIntegrationTest {
// then // then
exactlyTheseRbacGrantsAreReturned( exactlyTheseRbacGrantsAreReturned(
result, result,
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }"); "{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
} }
} }
@ -114,18 +113,19 @@ class RbacGrantRepositoryIntegrationTest {
// when // when
final var grant = RbacGrantEntity.builder() final var grant = RbacGrantEntity.builder()
.granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid) .granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true) .assumed(true)
.build(); .build();
final var attempt = attempt(em, () -> final var attempt = attempt(em, () ->
rbacGrantRepository.save(grant) rbacGrantRepository.save(grant)
); );
// then // then
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
@ -139,8 +139,8 @@ class RbacGrantRepositoryIntegrationTest {
// to find the uuids of we need to have access rights to these // to find the uuids of we need to have access rights to these
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
return new Given( return new Given(
createNewUser(), createNewUser(),
rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid() rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid()
); );
}).returnedValue(); }).returnedValue();
@ -150,24 +150,24 @@ class RbacGrantRepositoryIntegrationTest {
currentUser("aaa00@aaa.example.com"); currentUser("aaa00@aaa.example.com");
assumedRoles("package#aaa00.admin"); assumedRoles("package#aaa00.admin");
final var grant = RbacGrantEntity.builder() final var grant = RbacGrantEntity.builder()
.granteeUserUuid(given.arbitraryUser.getUuid()) .granteeUserUuid(given.arbitraryUser.getUuid())
.grantedRoleUuid(given.packageOwnerRoleUuid) .grantedRoleUuid(given.packageOwnerRoleUuid)
.assumed(true) .assumed(true)
.build(); .build();
rbacGrantRepository.save(grant); rbacGrantRepository.save(grant);
}); });
// then // then
attempt.assertExceptionWithRootCauseMessage( attempt.assertExceptionWithRootCauseMessage(
JpaSystemException.class, JpaSystemException.class,
"ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid "ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid
+ " forbidden for {package#aaa00.admin}"); + " forbidden for {package#aaa00.admin}");
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
// finally, we use the new user to make sure, no roles were granted // finally, we use the new user to make sure, no roles were granted
currentUser(given.arbitraryUser.getName()); currentUser(given.arbitraryUser.getName());
assertThat(rbacGrantRepository.findAll()) assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay) .extracting(RbacGrantEntity::toDisplay)
.hasSize(0); .hasSize(0);
}); });
} }
} }
@ -179,8 +179,8 @@ class RbacGrantRepositoryIntegrationTest {
public void customerAdmin_canRevokeSelfGrantedPackageAdminRole() { public void customerAdmin_canRevokeSelfGrantedPackageAdminRole() {
// given // given
final var grant = create(grant() final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("customer#aaa.admin") .byUser("admin@aaa.example.com").withAssumedRole("customer#aaa.admin")
.grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com")); .grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com"));
// when // when
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
@ -194,16 +194,16 @@ class RbacGrantRepositoryIntegrationTest {
assumedRoles("customer#aaa.admin"); assumedRoles("customer#aaa.admin");
assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull(); assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull();
assertThat(rbacGrantRepository.findAll()) assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::getGranteeUserName) .extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain("aac00@aac.example.com"); .doesNotContain("aac00@aac.example.com");
} }
@Test @Test
public void packageAdmin_canRevokeOwnPackageAdminRoleGrantedByAnotherAdminOfThatPackage() { public void packageAdmin_canRevokeOwnPackageAdminRoleGrantedByAnotherAdminOfThatPackage() {
// given // given
final var grant = create(grant() final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.admin") .byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.admin")
.grantingRole("package#aaa00.admin").toUser(createNewUser().getName())); .grantingRole("package#aaa00.admin").toUser(createNewUser().getName()));
// when // when
currentUser("aaa00@aaa.example.com"); currentUser("aaa00@aaa.example.com");
@ -217,16 +217,16 @@ class RbacGrantRepositoryIntegrationTest {
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
assumedRoles("customer#aaa.admin"); assumedRoles("customer#aaa.admin");
assertThat(rbacGrantRepository.findAll()) assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::getGranteeUserName) .extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain("aac00@aac.example.com"); .doesNotContain("aac00@aac.example.com");
} }
@Test @Test
public void packageAdmin_canNotRevokeOwnPackageAdminRoleGrantedByOwnerRoleOfThatPackage() { public void packageAdmin_canNotRevokeOwnPackageAdminRoleGrantedByOwnerRoleOfThatPackage() {
// given // given
final var grant = create(grant() final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.owner") .byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.owner")
.grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com")); .grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com"));
final var grantedByRole = rbacRoleRepository.findByRoleName("package#aaa00.owner"); final var grantedByRole = rbacRoleRepository.findByRoleName("package#aaa00.owner");
// when // when
@ -238,10 +238,10 @@ 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()
)); ));
} }
private RbacGrantEntity create(GrantBuilder with) { private RbacGrantEntity create(GrantBuilder with) {
@ -251,19 +251,19 @@ class RbacGrantRepositoryIntegrationTest {
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName(with.grantedRole).getUuid(); final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName(with.grantedRole).getUuid();
final var grant = RbacGrantEntity.builder() final var grant = RbacGrantEntity.builder()
.granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid) .granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true) .assumed(true)
.build(); .build();
final var grantAttempt = attempt(em, () -> final var grantAttempt = attempt(em, () ->
rbacGrantRepository.save(grant) rbacGrantRepository.save(grant)
); );
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
)); ));
return grant; return grant;
} }
@ -303,7 +303,7 @@ class RbacGrantRepositoryIntegrationTest {
private RbacUserEntity createNewUser() { private RbacUserEntity createNewUser() {
return rbacUserRepository.create( return rbacUserRepository.create(
new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com")); new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com"));
} }
void currentUser(final String currentUser) { void currentUser(final String currentUser) {
@ -318,9 +318,9 @@ class RbacGrantRepositoryIntegrationTest {
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) { void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
assertThat(actualResult) assertThat(actualResult)
.filteredOn(g -> !g.getGranteeUserName().startsWith("test-user-")) // ignore test-users created by other tests .filteredOn(g -> !g.getGranteeUserName().startsWith("test-user-")) // ignore test-users created by other tests
.extracting(RbacGrantEntity::toDisplay) .extracting(RbacGrantEntity::toDisplay)
.containsExactlyInAnyOrder(expectedGrant); .containsExactlyInAnyOrder(expectedGrant);
} }
} }