implements create new rbac-user and transacted JpaAttemp
This commit is contained in:
parent
dfc7162675
commit
41d3b678c4
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import org.openapitools.jackson.nullable.JsonNullableModule;
|
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -13,6 +14,6 @@ public class JsonObjectMapperConfiguration {
|
|||||||
@Primary
|
@Primary
|
||||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||||
return new Jackson2ObjectMapperBuilder()
|
return new Jackson2ObjectMapperBuilder()
|
||||||
.modules(new JsonNullableModule());
|
.modules(new JsonNullableModule(), new JavaTimeModule());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,13 @@ public class RestResponseEntityExceptionHandler
|
|||||||
return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message);
|
return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Throwable.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
||||||
|
final RuntimeException exc, final WebRequest request) {
|
||||||
|
final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
|
||||||
|
return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message);
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<HttpStatus> httpStatus(final String message) {
|
private Optional<HttpStatus> httpStatus(final String message) {
|
||||||
if (message.startsWith("ERROR: [")) {
|
if (message.startsWith("ERROR: [")) {
|
||||||
for (HttpStatus status : HttpStatus.values()) {
|
for (HttpStatus status : HttpStatus.values()) {
|
||||||
@ -48,10 +55,10 @@ public class RestResponseEntityExceptionHandler
|
|||||||
|
|
||||||
private static ResponseEntity<CustomErrorResponse> errorResponse(
|
private static ResponseEntity<CustomErrorResponse> errorResponse(
|
||||||
final WebRequest request,
|
final WebRequest request,
|
||||||
final HttpStatus conflict,
|
final HttpStatus httpStatus,
|
||||||
final String message) {
|
final String message) {
|
||||||
return new ResponseEntity<>(
|
return new ResponseEntity<>(
|
||||||
new CustomErrorResponse(request.getContextPath(), conflict, message), conflict);
|
new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String firstLine(final String message) {
|
private String firstLine(final String message) {
|
||||||
|
@ -17,4 +17,5 @@ public interface CustomerRepository extends Repository<CustomerEntity, UUID> {
|
|||||||
|
|
||||||
CustomerEntity save(final CustomerEntity entity);
|
CustomerEntity save(final CustomerEntity entity);
|
||||||
|
|
||||||
|
long count();
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,14 @@ import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserResource;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.map;
|
||||||
import static net.hostsharing.hsadminng.Mapper.mapList;
|
import static net.hostsharing.hsadminng.Mapper.mapList;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -19,15 +23,36 @@ public class RbacUserController implements RbacusersApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EntityManager em;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RbacUserRepository rbacUserRepository;
|
private RbacUserRepository rbacUserRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<RbacUserResource> createUser(
|
||||||
|
@RequestBody final RbacUserResource body
|
||||||
|
) {
|
||||||
|
if (body.getUuid() == null) {
|
||||||
|
body.setUuid(UUID.randomUUID());
|
||||||
|
}
|
||||||
|
final var saved = map(body, RbacUserEntity.class);
|
||||||
|
rbacUserRepository.create(saved);
|
||||||
|
final var uri =
|
||||||
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
|
.path("/api/rbac-users/{id}")
|
||||||
|
.buildAndExpand(saved.getUuid())
|
||||||
|
.toUri();
|
||||||
|
return ResponseEntity.created(uri).body(map(saved, RbacUserResource.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<List<RbacUserResource>> listUsers(
|
public ResponseEntity<List<RbacUserResource>> listUsers(
|
||||||
@RequestHeader(name = "current-user") String currentUserName,
|
@RequestHeader(name = "current-user") final String currentUserName,
|
||||||
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles,
|
||||||
@RequestParam(name="name", required = false) String userName
|
@RequestParam(name = "name", required = false) final String userName
|
||||||
) {
|
) {
|
||||||
context.setCurrentUser(currentUserName);
|
context.setCurrentUser(currentUserName);
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
@ -39,9 +64,9 @@ public class RbacUserController implements RbacusersApi {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions(
|
public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions(
|
||||||
@RequestHeader(name = "current-user") String currentUserName,
|
@RequestHeader(name = "current-user") final String currentUserName,
|
||||||
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles,
|
||||||
@PathVariable(name= "userName") String userName
|
@PathVariable(name = "userName") final String userName
|
||||||
) {
|
) {
|
||||||
context.setCurrentUser(currentUserName);
|
context.setCurrentUser(currentUserName);
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
|
@ -1,16 +1,41 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacuser;
|
package net.hostsharing.hsadminng.rbac.rbacuser;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.Repository;
|
import org.springframework.data.repository.Repository;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
|
public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
|
||||||
|
|
||||||
@Query("SELECT u FROM RbacUserEntity u WHERE :userName is null or u.name like concat(:userName, '%')")
|
@Query("""
|
||||||
List<RbacUserEntity> findByOptionalNameLike(final String userName);
|
select u from RbacUserEntity u
|
||||||
|
where :userName is null or u.name like concat(:userName, '%')
|
||||||
|
order by u.name
|
||||||
|
""")
|
||||||
|
List<RbacUserEntity> findByOptionalNameLike(String userName);
|
||||||
|
|
||||||
@Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true)
|
RbacUserEntity findByUuid(UUID uuid);
|
||||||
|
|
||||||
|
@Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true)
|
||||||
List<RbacUserPermission> findPermissionsOfUser(String userName);
|
List<RbacUserPermission> findPermissionsOfUser(String userName);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Can't use save/saveAndFlush from SpringData because the uuid is not generated on the entity level,
|
||||||
|
but explicitly, and then SpringData check's if it exists using an SQL SELECT.
|
||||||
|
And SQL SELECT needs a currentUser which we don't yet have in the case of self registration.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query(value = "insert into RBacUser_RV (uuid, name) values( :#{#newUser.uuid}, :#{#newUser.name})", nativeQuery = true)
|
||||||
|
void insert(@Param("newUser") final RbacUserEntity newUser);
|
||||||
|
|
||||||
|
default RbacUserEntity create(final RbacUserEntity rbacUserEntity) {
|
||||||
|
if (rbacUserEntity.getUuid() == null) {
|
||||||
|
rbacUserEntity.setUuid(UUID.randomUUID());
|
||||||
|
}
|
||||||
|
insert(rbacUserEntity);
|
||||||
|
return rbacUserEntity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,13 @@ components:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Error'
|
$ref: '#/components/schemas/Error'
|
||||||
Forbidden:
|
Forbidden:
|
||||||
description: The current user or none of the assumed or roles is granted access to the .
|
description: The current user or none of the assumed or roles is granted access to the resource.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
Conflict:
|
||||||
|
description: The request could not be completed due to a conflict with the current state of the target resource.
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
@ -21,19 +21,28 @@ get:
|
|||||||
items:
|
items:
|
||||||
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
||||||
"401":
|
"401":
|
||||||
description: if the 'current-user' cannot be identified
|
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
|
||||||
content:
|
|
||||||
'application/json':
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
|
||||||
"403":
|
"403":
|
||||||
description: if the 'current-user' is not allowed to assume any of the roles
|
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
|
||||||
from 'assumed-roles'
|
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- rbacusers
|
||||||
|
description: Create a new RBAC user.
|
||||||
|
operationId: createUser
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
type: array
|
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
||||||
items:
|
"409":
|
||||||
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
|
$ref: './api-definition/error-responses.yaml#/components/responses/Conflict'
|
||||||
|
|
||||||
|
@ -59,6 +59,22 @@ begin
|
|||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
create or replace function createRbacUser(refUuid uuid, userName varchar)
|
||||||
|
returns uuid
|
||||||
|
called on null input
|
||||||
|
language plpgsql as $$
|
||||||
|
begin
|
||||||
|
insert
|
||||||
|
into RbacReference as r (uuid, type)
|
||||||
|
values ( coalesce(refUuid, uuid_generate_v4()), 'RbacUser')
|
||||||
|
returning r.uuid into refUuid;
|
||||||
|
insert
|
||||||
|
into RbacUser (uuid, name)
|
||||||
|
values (refUuid, userName);
|
||||||
|
return refUuid;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
create or replace function findRbacUserId(userName varchar)
|
create or replace function findRbacUserId(userName varchar)
|
||||||
returns uuid
|
returns uuid
|
||||||
returns null on null input
|
returns null on null input
|
||||||
|
@ -9,11 +9,17 @@
|
|||||||
*/
|
*/
|
||||||
drop view if exists rbacrole_rv;
|
drop view if exists rbacrole_rv;
|
||||||
create or replace view rbacrole_rv as
|
create or replace view rbacrole_rv as
|
||||||
select DISTINCT r.*, o.objectTable,
|
select *
|
||||||
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
|
-- @formatter:off
|
||||||
from rbacrole as r
|
from (
|
||||||
join rbacobject as o on o.uuid=r.objectuuid
|
select r.*, o.objectTable,
|
||||||
where isGranted(currentSubjectIds(), r.uuid);
|
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
|
||||||
|
from rbacrole as r
|
||||||
|
join rbacobject as o on o.uuid = r.objectuuid
|
||||||
|
where isGranted(currentSubjectIds(), r.uuid)
|
||||||
|
) as unordered
|
||||||
|
-- @formatter:on
|
||||||
|
order by objectIdName;
|
||||||
grant all privileges on rbacrole_rv to restricted;
|
grant all privileges on rbacrole_rv to restricted;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
@ -27,13 +33,58 @@ grant all privileges on rbacrole_rv to restricted;
|
|||||||
*/
|
*/
|
||||||
drop view if exists RbacUser_rv;
|
drop view if exists RbacUser_rv;
|
||||||
create or replace view RbacUser_rv as
|
create or replace view RbacUser_rv as
|
||||||
select u.*
|
select distinct *
|
||||||
from RbacUser as u
|
-- @formatter:off
|
||||||
join RbacGrants as g on g.ascendantuuid = u.uuid
|
from (
|
||||||
join rbacrole_rv as r on r.uuid = g.descendantuuid;
|
select usersInRolesOfCurrentUser.*
|
||||||
|
from RbacUser as usersInRolesOfCurrentUser
|
||||||
|
join RbacGrants as g on g.ascendantuuid = usersInRolesOfCurrentUser.uuid
|
||||||
|
join rbacrole_rv as r on r.uuid = g.descendantuuid
|
||||||
|
union
|
||||||
|
select users.*
|
||||||
|
from RbacUser as users
|
||||||
|
where cardinality(assumedRoles()) = 0 and currentUserId() = users.uuid
|
||||||
|
) as unordered
|
||||||
|
-- @formatter:on
|
||||||
|
order by unordered.name;
|
||||||
grant all privileges on RbacUser_rv to restricted;
|
grant all privileges on RbacUser_rv to restricted;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset rbac-views-USER-RV-INSERT-TRIGGER:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
Instead of insert trigger function for RbacUser_rv.
|
||||||
|
*/
|
||||||
|
create or replace function insertRbacUser()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql as $$
|
||||||
|
declare
|
||||||
|
refUuid uuid;
|
||||||
|
newUser RbacUser;
|
||||||
|
begin
|
||||||
|
insert
|
||||||
|
into RbacReference as r (uuid, type)
|
||||||
|
values( new.uuid, 'RbacUser')
|
||||||
|
returning r.uuid into refUuid;
|
||||||
|
insert
|
||||||
|
into RbacUser (uuid, name)
|
||||||
|
values (refUuid, new.name)
|
||||||
|
returning * into newUser;
|
||||||
|
return newUser;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Creates an instead of insert trigger for the RbacUser_rv view.
|
||||||
|
*/
|
||||||
|
create trigger insertRbacUser_Trigger
|
||||||
|
instead of insert
|
||||||
|
on RbacUser_rv
|
||||||
|
for each row
|
||||||
|
execute function insertRbacUser();
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset rbac-views-OWN-GRANTED-PERMISSIONS-VIEW:1 endDelimiter:--//
|
--changeset rbac-views-OWN-GRANTED-PERMISSIONS-VIEW:1 endDelimiter:--//
|
||||||
|
@ -36,19 +36,21 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
public void hostsharingAdmin_withoutAssumedRole_canCreateNewCustomer() {
|
public void hostsharingAdmin_withoutAssumedRole_canCreateNewCustomer() {
|
||||||
// given
|
// given
|
||||||
currentUser("mike@hostsharing.net");
|
currentUser("mike@hostsharing.net");
|
||||||
|
final var count = customerRepository.count();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
|
||||||
final var attempt = attempt(em, () -> {
|
final var result = attempt(em, () -> {
|
||||||
final var newCustomer = new CustomerEntity(
|
final var newCustomer = new CustomerEntity(
|
||||||
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
||||||
return customerRepository.save(newCustomer);
|
return customerRepository.save(newCustomer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(attempt.wasSuccessful()).isTrue();
|
assertThat(result.wasSuccessful()).isTrue();
|
||||||
assertThat(attempt.returnedResult()).isNotNull().extracting(CustomerEntity::getUuid).isNotNull();
|
assertThat(result.returnedValue()).isNotNull().extracting(CustomerEntity::getUuid).isNotNull();
|
||||||
assertThatCustomerIsPersisted(attempt.returnedResult());
|
assertThatCustomerIsPersisted(result.returnedValue());
|
||||||
|
assertThat(customerRepository.count()).isEqualTo(count + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -58,14 +60,14 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
assumedRoles("customer#aaa.admin");
|
assumedRoles("customer#aaa.admin");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var attempt = attempt(em, () -> {
|
final var result = attempt(em, () -> {
|
||||||
final var newCustomer = new CustomerEntity(
|
final var newCustomer = new CustomerEntity(
|
||||||
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
||||||
return customerRepository.save(newCustomer);
|
return customerRepository.save(newCustomer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// then
|
// then
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
PersistenceException.class,
|
PersistenceException.class,
|
||||||
"add-customer not permitted for customer#aaa.admin");
|
"add-customer not permitted for customer#aaa.admin");
|
||||||
}
|
}
|
||||||
@ -76,14 +78,14 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
currentUser("admin@aaa.example.com");
|
currentUser("admin@aaa.example.com");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var attempt = attempt(em, () -> {
|
final var result = attempt(em, () -> {
|
||||||
final var newCustomer = new CustomerEntity(
|
final var newCustomer = new CustomerEntity(
|
||||||
UUID.randomUUID(), "yyy", 90002, "admin@yyy.example.com");
|
UUID.randomUUID(), "yyy", 90002, "admin@yyy.example.com");
|
||||||
return customerRepository.save(newCustomer);
|
return customerRepository.save(newCustomer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// then
|
// then
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
PersistenceException.class,
|
PersistenceException.class,
|
||||||
"add-customer not permitted for admin@aaa.example.com");
|
"add-customer not permitted for admin@aaa.example.com");
|
||||||
|
|
||||||
@ -152,12 +154,12 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
assumedRoles("package#aab00.admin");
|
assumedRoles("package#aab00.admin");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
|
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
|
||||||
}
|
}
|
||||||
@ -166,11 +168,11 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
void unknownUser_withoutAssumedRole_cannotViewAnyCustomers() {
|
void unknownUser_withoutAssumedRole_cannotViewAnyCustomers() {
|
||||||
currentUser("unknown@example.org");
|
currentUser("unknown@example.org");
|
||||||
|
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
||||||
|
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
@ -181,11 +183,11 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
currentUser("unknown@example.org");
|
currentUser("unknown@example.org");
|
||||||
assumedRoles("customer#aaa.admin");
|
assumedRoles("customer#aaa.admin");
|
||||||
|
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
() -> customerRepository.findCustomerByOptionalPrefixLike(null));
|
||||||
|
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
|
@ -85,12 +85,12 @@ class PackageRepositoryIntegrationTest {
|
|||||||
assumedRoles("package#aab00.admin");
|
assumedRoles("package#aab00.admin");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> packageRepository.findAllByOptionalNameLike(null));
|
() -> packageRepository.findAllByOptionalNameLike(null));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
|
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
|
||||||
}
|
}
|
||||||
@ -99,11 +99,11 @@ class PackageRepositoryIntegrationTest {
|
|||||||
void unknownUser_withoutAssumedRole_cannotViewAnyPackages() {
|
void unknownUser_withoutAssumedRole_cannotViewAnyPackages() {
|
||||||
currentUser("unknown@example.org");
|
currentUser("unknown@example.org");
|
||||||
|
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> packageRepository.findAllByOptionalNameLike(null));
|
() -> packageRepository.findAllByOptionalNameLike(null));
|
||||||
|
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
@ -114,11 +114,11 @@ class PackageRepositoryIntegrationTest {
|
|||||||
currentUser("unknown@example.org");
|
currentUser("unknown@example.org");
|
||||||
assumedRoles("customer#aaa.admin");
|
assumedRoles("customer#aaa.admin");
|
||||||
|
|
||||||
final var attempt = attempt(
|
final var result = attempt(
|
||||||
em,
|
em,
|
||||||
() -> packageRepository.findAllByOptionalNameLike(null));
|
() -> packageRepository.findAllByOptionalNameLike(null));
|
||||||
|
|
||||||
attempt.assertExceptionWithRootCauseMessage(
|
result.assertExceptionWithRootCauseMessage(
|
||||||
JpaSystemException.class,
|
JpaSystemException.class,
|
||||||
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
@ -141,7 +141,6 @@ class PackageRepositoryIntegrationTest {
|
|||||||
.isEmpty();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void exactlyThesePackagesAreReturned(final List<PackageEntity> actualResult, final String... packageNames) {
|
void exactlyThesePackagesAreReturned(final List<PackageEntity> actualResult, final String... packageNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(PackageEntity::getName)
|
.extracting(PackageEntity::getName)
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacuser;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
classes = HsadminNgApplication.class
|
||||||
|
)
|
||||||
|
@Transactional
|
||||||
|
class RbacUserControllerAcceptanceTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private Integer port;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RbacUserRepository rbacUserRepository;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ApiRbacUsersGet {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hostsharingAdmin_withoutAssumedRole_canViewAllUsers() {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
RestAssured
|
||||||
|
.given()
|
||||||
|
.header("current-user", "mike@hostsharing.net")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/rbac-users")
|
||||||
|
.then().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("[0].name", is("aaa00@aaa.example.com"))
|
||||||
|
.body("[1].name", is("aaa01@aaa.example.com"))
|
||||||
|
.body("[2].name", is("aaa02@aaa.example.com"))
|
||||||
|
.body("size()", is(14));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hostsharingAdmin_withoutAssumedRole_canViewAllUsersByName() {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
RestAssured
|
||||||
|
.given()
|
||||||
|
.header("current-user", "mike@hostsharing.net")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/rbac-users?name=aac")
|
||||||
|
.then().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("[0].name", is("aac00@aac.example.com"))
|
||||||
|
.body("[1].name", is("aac01@aac.example.com"))
|
||||||
|
.body("[2].name", is("aac02@aac.example.com"))
|
||||||
|
.body("size()", is(3));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customerAdmin_withoutAssumedRole_canViewUsersInItsRealm() {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
RestAssured
|
||||||
|
.given()
|
||||||
|
.header("current-user", "admin@aab.example.com")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/rbac-users")
|
||||||
|
.then().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("[0].name", is("aab00@aab.example.com"))
|
||||||
|
.body("[1].name", is("aab01@aab.example.com"))
|
||||||
|
.body("[2].name", is("aab02@aab.example.com"))
|
||||||
|
.body("[3].name", is("admin@aab.example.com"))
|
||||||
|
.body("size()", is(4));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packetAdmin_withoutAssumedRole_canViewAllUsersOfItsPackage() {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
RestAssured
|
||||||
|
.given()
|
||||||
|
.header("current-user", "aaa01@aaa.example.com")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/rbac-users")
|
||||||
|
.then().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("[0].name", is("aaa01@aaa.example.com"))
|
||||||
|
.body("size()", is(1));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ApiRbacUsersPost {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anybody_canCreateANewUser() {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
final var location = RestAssured
|
||||||
|
.given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": "new-user@example.com"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/rbac-users")
|
||||||
|
.then().assertThat()
|
||||||
|
.statusCode(201)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("name", is("new-user@example.com"))
|
||||||
|
.header("Location", startsWith("http://localhost"))
|
||||||
|
.extract().header("Location");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// finally, the user can view its own record
|
||||||
|
final var newUserUuid = UUID.fromString(
|
||||||
|
location.substring(location.lastIndexOf('/') + 1));
|
||||||
|
context.setCurrentUser("new-user@example.com");
|
||||||
|
assertThat(rbacUserRepository.findByUuid(newUserUuid))
|
||||||
|
.extracting(RbacUserEntity::getName).isEqualTo("new-user@example.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacuser;
|
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.rbac.rbacrole.TestRbacRole.*;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.rbacuser.TestRbacUser.userAaa;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.rbacuser.TestRbacUser.userBbb;
|
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
@WebMvcTest(RbacUserController.class)
|
|
||||||
class RbacUserControllerRestTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
MockMvc mockMvc;
|
|
||||||
@MockBean
|
|
||||||
Context contextMock;
|
|
||||||
@MockBean
|
|
||||||
RbacUserRepository rbacUserRepository;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void willListAllUsers() throws Exception {
|
|
||||||
|
|
||||||
// given
|
|
||||||
when(rbacUserRepository.findByOptionalNameLike(null)).thenReturn(
|
|
||||||
List.of(userAaa, userBbb));
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.get("/api/rbac-users")
|
|
||||||
.header("current-user", "mike@hostsharing.net")
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$", hasSize(2)))
|
|
||||||
.andExpect(jsonPath("$[0].name", is(userAaa.getName())))
|
|
||||||
.andExpect(jsonPath("$[1].name", is(userBbb.getName())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void willListUsersByName() throws Exception {
|
|
||||||
|
|
||||||
// given
|
|
||||||
when(rbacUserRepository.findByOptionalNameLike("admin@aaa")).thenReturn(
|
|
||||||
List.of(userAaa));
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.get("/api/rbac-users")
|
|
||||||
.param("name", "admin@aaa")
|
|
||||||
.header("current-user", "mike@hostsharing.net")
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$", hasSize(1)))
|
|
||||||
.andExpect(jsonPath("$[0].name", is(userAaa.getName())));
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,21 +2,26 @@ package net.hostsharing.hsadminng.rbac.rbacuser;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.test.Array;
|
import net.hostsharing.test.Array;
|
||||||
|
import net.hostsharing.test.JpaAttempt;
|
||||||
import org.junit.jupiter.api.Nested;
|
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.orm.jpa.DataJpaTest;
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.test.annotation.Commit;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static net.hostsharing.test.JpaAttempt.attempt;
|
import static net.hostsharing.test.JpaAttempt.attempt;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
@ComponentScan(basePackageClasses = { Context.class, RbacUserRepository.class })
|
@ComponentScan(basePackageClasses = { RbacUserRepository.class, Context.class, JpaAttempt.class })
|
||||||
class RbacUserRepositoryIntegrationTest {
|
class RbacUserRepositoryIntegrationTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -25,8 +30,59 @@ class RbacUserRepositoryIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
RbacUserRepository rbacUserRepository;
|
RbacUserRepository rbacUserRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
@Autowired EntityManager em;
|
@Autowired EntityManager em;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class CreateUser {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anyoneCanCreateTheirOwnUser() {
|
||||||
|
// given
|
||||||
|
final var givenNewUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = rbacUserRepository.create(
|
||||||
|
new RbacUserEntity(null, givenNewUserName));
|
||||||
|
|
||||||
|
// then the persisted user is returned
|
||||||
|
assertThat(result).isNotNull().extracting(RbacUserEntity::getName).isEqualTo(givenNewUserName);
|
||||||
|
|
||||||
|
// and the new user entity can be fetched by the user itself
|
||||||
|
currentUser(givenNewUserName);
|
||||||
|
assertThat(em.find(RbacUserEntity.class, result.getUuid()))
|
||||||
|
.isNotNull().extracting(RbacUserEntity::getName).isEqualTo(givenNewUserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Commit
|
||||||
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
|
void anyoneCanCreateTheirOwnUser_committed() {
|
||||||
|
|
||||||
|
// given:
|
||||||
|
final var givenUuid = UUID.randomUUID();
|
||||||
|
final var newUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||||
|
|
||||||
|
// when:
|
||||||
|
final var result = jpaAttempt.transacted(() -> {
|
||||||
|
currentUser("admin@aaa.example.com");
|
||||||
|
return rbacUserRepository.create(new RbacUserEntity(givenUuid, newUserName));
|
||||||
|
});
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertThat(result.wasSuccessful()).isTrue();
|
||||||
|
assertThat(result.returnedValue()).isNotNull()
|
||||||
|
.extracting(RbacUserEntity::getUuid).isEqualTo(givenUuid);
|
||||||
|
jpaAttempt.transacted(() -> {
|
||||||
|
currentUser(newUserName);
|
||||||
|
assertThat(em.find(RbacUserEntity.class, givenUuid))
|
||||||
|
.isNotNull().extracting(RbacUserEntity::getName).isEqualTo(newUserName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class FindByOptionalNameLike {
|
class FindByOptionalNameLike {
|
||||||
|
|
||||||
@ -210,7 +266,8 @@ class RbacUserRepositoryIntegrationTest {
|
|||||||
final var result = rbacUserRepository.findPermissionsOfUser("admin@aaa.example.com");
|
final var result = rbacUserRepository.findPermissionsOfUser("admin@aaa.example.com");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseRbacPermissionsAreReturned(result,
|
exactlyTheseRbacPermissionsAreReturned(
|
||||||
|
result,
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
"customer#aaa.admin -> customer#aaa: add-package",
|
"customer#aaa.admin -> customer#aaa: add-package",
|
||||||
"customer#aaa.admin -> customer#aaa: view",
|
"customer#aaa.admin -> customer#aaa: view",
|
||||||
@ -256,7 +313,8 @@ class RbacUserRepositoryIntegrationTest {
|
|||||||
final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com");
|
final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseRbacPermissionsAreReturned(result,
|
exactlyTheseRbacPermissionsAreReturned(
|
||||||
|
result,
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
"customer#aaa.tenant -> customer#aaa: view",
|
"customer#aaa.tenant -> customer#aaa: view",
|
||||||
// "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin!
|
// "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin!
|
||||||
@ -288,7 +346,8 @@ class RbacUserRepositoryIntegrationTest {
|
|||||||
final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com");
|
final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseRbacPermissionsAreReturned(result,
|
exactlyTheseRbacPermissionsAreReturned(
|
||||||
|
result,
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
"customer#aaa.tenant -> customer#aaa: view",
|
"customer#aaa.tenant -> customer#aaa: view",
|
||||||
// "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin!
|
// "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin!
|
||||||
@ -323,7 +382,6 @@ class RbacUserRepositoryIntegrationTest {
|
|||||||
.containsExactlyInAnyOrder();
|
.containsExactlyInAnyOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void exactlyTheseRbacPermissionsAreReturned(
|
void exactlyTheseRbacPermissionsAreReturned(
|
||||||
final List<RbacUserPermission> actualResult,
|
final List<RbacUserPermission> actualResult,
|
||||||
final String... expectedRoleNames) {
|
final String... expectedRoleNames) {
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package net.hostsharing.test;
|
package net.hostsharing.test;
|
||||||
|
|
||||||
import junit.framework.AssertionFailedError;
|
import junit.framework.AssertionFailedError;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.NestedExceptionUtils;
|
import org.springframework.core.NestedExceptionUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -13,69 +17,98 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
* Wraps the 'when' part of a DataJpaTest to improve readability of tests.
|
* Wraps the 'when' part of a DataJpaTest to improve readability of tests.
|
||||||
* <p>
|
* <p>
|
||||||
* It
|
* It
|
||||||
* - makes sure that the SQL code is actually performed (em.flush()),
|
* <li> makes sure that the SQL code is actually performed (em.flush()),
|
||||||
* - if any exception is throw, it's caught and stored,
|
* <li> if any exception is throw, it's caught and stored,
|
||||||
* - makes the result available for assertions,
|
* <li> makes the result available for assertions,
|
||||||
* - cleans the JPA first level cache to force assertions read from the database, not just cache,
|
* <li> cleans the JPA first level cache to force assertions read from the database, not just cache,
|
||||||
* - offers some assertions based on the exception.
|
* <li> offers some assertions based on the exception.
|
||||||
* *
|
* </p>
|
||||||
*
|
* <p>
|
||||||
* @param <T> success result type
|
* To run in same transaction as caller, use the static `attempt` method,
|
||||||
|
* to run in a new transaction, inject this class and use the instance `transacted` methods.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class JpaAttempt<T> {
|
@Service
|
||||||
|
public class JpaAttempt {
|
||||||
|
|
||||||
private T result = null;
|
@Autowired
|
||||||
private RuntimeException exception = null;
|
private final EntityManager em;
|
||||||
|
|
||||||
private String firstRootCauseMessageLineOf(final RuntimeException exception) {
|
public JpaAttempt(final EntityManager em) {
|
||||||
final var rootCause = NestedExceptionUtils.getRootCause(exception);
|
this.em = em;
|
||||||
return Optional.ofNullable(rootCause)
|
|
||||||
.map(Throwable::getMessage)
|
|
||||||
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> JpaAttempt<T> attempt(final EntityManager em, final Supplier<T> code) {
|
public static <T> JpaResult<T> attempt(final EntityManager em, final Supplier<T> code) {
|
||||||
return new JpaAttempt<>(em, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JpaAttempt(final EntityManager em, final Supplier<T> code) {
|
|
||||||
try {
|
try {
|
||||||
result = code.get();
|
final var result = new JpaResult<T>(code.get(), null);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
return result;
|
||||||
} catch (RuntimeException exc) {
|
} catch (RuntimeException exc) {
|
||||||
exception = exc;
|
return new JpaResult<T>(null, exc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean wasSuccessful() {
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
return exception == null;
|
public <T> JpaResult<T> transacted(final Supplier<T> code) {
|
||||||
|
return attempt(em, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T returnedResult() {
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
return result;
|
public void transacted(final Runnable code) {
|
||||||
|
attempt(em, () -> {
|
||||||
|
code.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeException caughtException() {
|
public static class JpaResult<T> {
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
final T result;
|
||||||
public <E extends RuntimeException> E caughtException(final Class<E> expectedExceptionClass) {
|
final RuntimeException exception;
|
||||||
if (expectedExceptionClass.isAssignableFrom(exception.getClass())) {
|
|
||||||
return (E) exception;
|
public JpaResult(final T result, final RuntimeException exception) {
|
||||||
|
this.result = result;
|
||||||
|
this.exception = exception;
|
||||||
}
|
}
|
||||||
throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void assertExceptionWithRootCauseMessage(
|
public boolean wasSuccessful() {
|
||||||
final Class<? extends RuntimeException> expectedExceptionClass,
|
return exception == null;
|
||||||
final String... expectedRootCauseMessages) {
|
}
|
||||||
assertThat(wasSuccessful()).isFalse();
|
|
||||||
final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
|
public T returnedValue() {
|
||||||
for ( String expectedRootCauseMessage: expectedRootCauseMessages ) {
|
return result;
|
||||||
assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage);
|
}
|
||||||
|
|
||||||
|
public RuntimeException caughtException() {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <E extends RuntimeException> E caughtException(final Class<E> expectedExceptionClass) {
|
||||||
|
if (expectedExceptionClass.isAssignableFrom(exception.getClass())) {
|
||||||
|
return (E) exception;
|
||||||
|
}
|
||||||
|
throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertExceptionWithRootCauseMessage(
|
||||||
|
final Class<? extends RuntimeException> expectedExceptionClass,
|
||||||
|
final String... expectedRootCauseMessages) {
|
||||||
|
assertThat(wasSuccessful()).isFalse();
|
||||||
|
final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
|
||||||
|
for (String expectedRootCauseMessage : expectedRootCauseMessages) {
|
||||||
|
assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstRootCauseMessageLineOf(final RuntimeException exception) {
|
||||||
|
final var rootCause = NestedExceptionUtils.getRootCause(exception);
|
||||||
|
return Optional.ofNullable(rootCause)
|
||||||
|
.map(Throwable::getMessage)
|
||||||
|
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ spring:
|
|||||||
hibernate:
|
hibernate:
|
||||||
default_schema: public
|
default_schema: public
|
||||||
dialect: net.hostsharing.hsadminng.config.PostgreSQL95CustomDialect
|
dialect: net.hostsharing.hsadminng.config.PostgreSQL95CustomDialect
|
||||||
|
format_sql: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
show-sql: true
|
show-sql: true
|
||||||
@ -29,3 +30,6 @@ spring:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
liquibase: INFO
|
liquibase: INFO
|
||||||
|
org.hibernate.SQL: DEBUG
|
||||||
|
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||||
|
X.org.springframework.jdbc.core: TRACE
|
||||||
|
Loading…
Reference in New Issue
Block a user