1
0

add /api/rbac/context + /api/hs/accounts/current endpoints (#189)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/189
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-08-21 12:45:59 +02:00
parent 60028697d6
commit 5a5c1466b0
51 changed files with 1034 additions and 129 deletions
@@ -83,6 +83,7 @@ public class ArchitectureTest {
"..mapper",
"..ping",
"..rbac",
"..rbac.context",
"..rbac.generator",
"..rbac.subject",
"..rbac.grant",
@@ -238,7 +239,8 @@ public class ArchitectureTest {
"..hs.office.debitor..",
"..hs.office.membership..",
"..hs.migration..",
"..hs.hosting.asset.."
"..hs.hosting.asset..",
"..hs.accounts.."
);
@ArchTest
@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.accounts;
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.MessagesResourceConfig;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
@@ -26,20 +27,31 @@ import jakarta.persistence.EntityManagerFactory;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON;
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON;
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsCredentialsController.class)
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class, MessageTranslator.class })
@Import({
StrictMapper.class,
JsonObjectMapperConfiguration.class,
DisableSecurityConfig.class,
// HOWTO: test i18n translations
MessagesResourceConfig.class,
MessageTranslator.class })
@ActiveProfiles("test")
class HsCredentialsControllerRestTest {
@@ -79,6 +91,45 @@ class HsCredentialsControllerRestTest {
@MockitoBean
CredentialContextResourceToEntityMapper contextMapper;
@Test
void shouldFetchCurrentLoginUser() throws Exception {
// given
final UUID currentSubjectUuid = UUID.randomUUID();
given(contextMock.fetchCurrentSubjectUuid()).willReturn(currentSubjectUuid);
given(contextMock.isGlobalAdmin()).willReturn(true);
given(subjectRepo.findByUuid(currentSubjectUuid)).willReturn(
RbacSubjectEntity.builder().uuid(currentSubjectUuid).name("test-user").build()
);
given(credentialsRepo.findByUuid(currentSubjectUuid)).willReturn(
Optional.of(HsCredentialsEntity.builder()
.uuid(currentSubjectUuid)
.person(HsOfficePersonRbacEntity.builder()
.uuid(PERSON_UUID)
.personType(NATURAL_PERSON)
.familyName("Miller")
.givenName("Steph")
.build())
.subject(RbacSubjectEntity.builder().name("steph-miller").build())
.build())
);
// when
mockMvc.perform(MockMvcRequestBuilders
.get("/api/hs/accounts/current")
.header("Authorization", "Bearer test")
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
// then
.andExpect(status().isOk())
.andExpect(jsonPath("$.subject.uuid").value(currentSubjectUuid.toString()))
.andExpect(jsonPath("$.subject.name").value("test-user"))
.andExpect(jsonPath("$.person.uuid").value(PERSON_UUID.toString()))
.andExpect(jsonPath("$.person.familyName").value("Miller"))
.andExpect(jsonPath("$.person.givenName").value("Steph"))
.andExpect(jsonPath("$.globalAdmin").value(true));
}
@Test
void shouldFilterInvalidContextsRegardingNonNaturalPerson() throws Exception {
// given
@@ -123,7 +174,95 @@ class HsCredentialsControllerRestTest {
}
@Test
void patchCredentialsUsed() throws Exception {
void shouldRejectCreatingCredentialsForUnrepresentedPerson() throws Exception {
// given
final var personUuid = UUID.randomUUID();
final AtomicReference<RbacSubjectEntity> createdSubject = new AtomicReference<>();
given(subjectRepo.create(any())).willAnswer(invocation -> {
final var passedEntity = (RbacSubjectEntity) invocation.getArgument(0);
passedEntity.setUuid(UUID.randomUUID());
createdSubject.set(passedEntity); // Capture the instance
return passedEntity;
});
given(contextMock.fetchCurrentSubject()).willAnswer(invocation -> createdSubject.get().getName());
given(subjectRepo.findByUuid(any())).willAnswer(invocation -> createdSubject.get());
given(rbacPersonRepo.findByUuid(personUuid)).willReturn(Optional.of(
HsOfficePersonRbacEntity.builder().uuid(personUuid).personType(NATURAL_PERSON).build()
));
given(rbacPersonRepo.findPersonsrepresentedByPersonWithUuid(personUuid)).willReturn(List.of(
// some persons, but not the one from the login-user itself
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(NATURAL_PERSON).build(),
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(LEGAL_PERSON).build()
));
final var givenCredentialsUuid = UUID.randomUUID();
final var contextForNP = HsCredentialsContextRealEntity.builder()
.uuid(UUID.randomUUID())
.type("HSADMIN")
.qualifier("prod")
.onlyForNaturalPersons(true)
.build();
final var contextForAll = HsCredentialsContextRealEntity.builder()
.uuid(UUID.randomUUID())
.type("SSH")
.qualifier("prod")
.onlyForNaturalPersons(false)
.build();
final var credentialsEntity = HsCredentialsEntity.builder()
.uuid(givenCredentialsUuid)
.person(HsOfficePersonRbacEntity.builder()
.uuid(PERSON_UUID)
.personType(LEGAL_PERSON)
.build())
.subject(RbacSubjectEntity.builder().name("some-nickname").build())
.loginContexts(Set.of(contextForNP, contextForAll))
.build();
when(credentialsRepo.findByUuid(givenCredentialsUuid))
.thenReturn(Optional.of(credentialsEntity));
// when
mockMvc.perform(MockMvcRequestBuilders
.post("/api/hs/accounts/credentials")
.header("Authorization", "Bearer test")
// HOWTO: test i18n translations
.header("Accept-Language", "de")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"person.uuid": "${personUuid}",
"nickname": "${nickname}",
"active": true,
"globalUid": 30001,
"globalGid": 40001,
"contexts": [
{
"uuid" : "11111111-1111-1111-1111-111111111111",
"type" : "HSADMIN",
"qualifier" : "prod",
"onlyForNaturalPersons" : true,
"publicAccess" : true
}
]
}
"""
.replace("${personUuid}", personUuid.toString())
.replace("${nickname}", "new-user")
)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
// then
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("$.message", containsString(
"Zugriff verweigert: personUuid \"${personUuid}\" wird von der eingeloggten Person nicht repräsentiert"
.replace("${personUuid}", personUuid.toString()))));
}
@Test
void markCredentialsAsUsed() throws Exception {
// given
final var givenCredentialsUuid = UUID.randomUUID();
@@ -1,9 +1,14 @@
package net.hostsharing.hsadminng.hs.accounts;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.hibernate.TransientObjectException;
import org.junit.jupiter.api.BeforeEach;
@@ -22,15 +27,18 @@ import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.Set;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.REPRESENTATIVE;
import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
@DataJpaTest
@Tag("generalIntegrationTest")
@Import({ Context.class, JpaAttempt.class })
class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
private static final String SUPERUSER_FRAN_SUBJECT_NAME = "superuser-fran@hostsharing.net";
private static final String USER_DREW_SUBJECT_NAME = "selfregistered-user-drew@hostsharing.org";
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
@@ -41,6 +49,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
@MockitoBean
HttpServletRequest request;
@Autowired
private HsOfficePersonRealRepository personRepo;
@Autowired
private HsCredentialsRepository credentialsRepository;
@@ -74,7 +85,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
final var rowsBefore = query.getResultList();
// then
assertThat(rowsBefore).as("hs_accounts.credentials_hv only contain no rows for a timestamp before test data creation").hasSize(0);
assertThat(rowsBefore)
.as("hs_accounts.credentials_hv only contain no rows for a timestamp before test data creation")
.hasSize(0);
// and when
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
@@ -82,7 +95,53 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
final var rowsAfter = query.getResultList();
// then
assertThat(rowsAfter).as("hs_accounts.credentials_hv should now contain the test-data rows for the current timestamp").hasSize(2);
assertThat(rowsAfter)
.as("hs_accounts.credentials_hv should now contain the test-data rows for the current timestamp")
.hasSize(3);
}
@Test
void representativeShouldFindOwnAndRepresentedCredentialsByCurrentSubject() {
// given
final var firstGmbHPerson = givenPerson("First GmbH");
givenRelation(REPRESENTATIVE)
.withAnchorPersonLike(firstGmbHPerson)
.withHolder(drewPerson)
.withContact("some test contact");
givenCredentials()
.forSubject("first-gmbh")
.forPerson(firstGmbHPerson)
.withEMailAddress("first-gmbh@example.com");
// when
final var foundCredentials = attempt(
em, () -> {
context(USER_DREW_SUBJECT_NAME);
return credentialsRepository.findByCurrentSubject();
})
.assertNotNull().returnedValue();
// then
assertThat(foundCredentials).hasSize(2)
.map(HsCredentialsEntity::getEmailAddress)
.containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com");
}
@Test
void globalAdminShouldFindOnlyOwnCredentialsByCurrentSubject() {
// when
final var foundCredentials = attempt(
em, () -> {
context(SUPERUSER_FRAN_SUBJECT_NAME);
return credentialsRepository.findByCurrentSubject();
})
.assertNotNull().returnedValue();
// then
assertThat(foundCredentials).hasSize(1)
.map(HsCredentialsEntity::getEmailAddress)
.containsExactlyInAnyOrder("fran@example.com");
}
@Test
@@ -101,28 +160,28 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
.orElseThrow();
final var newCredentials = HsCredentialsEntity.builder()
.subject(drewSubject)
.person(drewPerson)
.subject(testUserSubject)
.person(testUserPerson)
.active(true)
.emailAddress("drew.new@example.com")
.globalUid(2001)
.globalGid(2001)
.emailAddress("test-user@example.com")
.globalUid(2011)
.globalGid(2011)
.loginContexts(mutableSetOf(existingContext))
.build();
// when
credentialsRepository.save(newCredentials);
toCleanup(credentialsRepository.save(newCredentials));
em.flush();
em.clear();
// then
final var foundEntityOptional = credentialsRepository.findByUuid(drewSubject.getUuid());
final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid());
assertThat(foundEntityOptional).isPresent();
final var foundEntity = foundEntityOptional.get();
assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com");
assertThat(foundEntity.getEmailAddress()).isEqualTo("test-user@example.com");
assertThat(foundEntity.isActive()).isTrue();
assertThat(foundEntity.getVersion()).isEqualTo(0); // Initial version
assertThat(foundEntity.getGlobalUid()).isEqualTo(2001);
assertThat(foundEntity.getGlobalUid()).isEqualTo(2011);
assertThat(foundEntity.getLoginContexts()).hasSize(1)
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
@@ -240,4 +299,88 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
private <T> Set<T> mutableSetOf(final T... elements) {
return new HashSet<T>(Set.of(elements));
}
private HsOfficePersonRealEntity givenPerson(String personName) {
return personRepo.findPersonByOptionalNameLike(personName).getFirst();
}
private RelationBuilder givenRelation(HsOfficeRelationType relationType) {
return new RelationBuilder(relationType);
}
private CredentialsBuilder givenCredentials() {
return new CredentialsBuilder();
}
private class RelationBuilder {
private final HsOfficeRelationType relationType;
private HsOfficePersonRealEntity anchorPerson;
private HsOfficePersonRbacEntity holderPerson;
private HsOfficeContactRealEntity contact;
public RelationBuilder(HsOfficeRelationType relationType) {
this.relationType = relationType;
}
public RelationBuilder withAnchorPersonLike(HsOfficePersonRealEntity anchorPerson) {
this.anchorPerson = anchorPerson;
return this;
}
public RelationBuilder withHolder(HsOfficePersonRbacEntity holderPerson) {
this.holderPerson = holderPerson;
return this;
}
public HsOfficeRelationRealEntity withContact(String caption) {
this.contact = HsOfficeContactRealEntity.builder()
.caption(caption)
.build();
em.persist(contact);
final var relation = HsOfficeRelationRealEntity.builder()
.type(relationType)
.anchor(anchorPerson)
.holder(em.getReference(HsOfficePersonRealEntity.class, holderPerson.getUuid()))
.contact(contact)
.build();
em.persist(relation);
em.flush();
return relation;
}
}
private class CredentialsBuilder {
private RbacSubjectEntity subject;
private HsOfficePersonRealEntity person;
public CredentialsBuilder forSubject(String subjectName) {
this.subject = RbacSubjectEntity.builder()
.name(subjectName)
.build();
em.persist(subject);
toCleanup(subject);
return this;
}
public CredentialsBuilder forPerson(HsOfficePersonRealEntity person) {
this.person = person;
return this;
}
public HsCredentialsEntity withEMailAddress(String emailAddress) {
final var credentials = HsCredentialsEntity.builder()
.uuid(subject.getUuid())
.subject(subject)
.person(em.find(HsOfficePersonRbacEntity.class, person.getUuid()))
.emailAddress(emailAddress)
.active(true)
.build();
em.persist(credentials);
toCleanup(credentials);
em.flush();
return credentials;
}
}
}
@@ -49,6 +49,42 @@ class CredentialsScenarioTests extends ScenarioTest {
@Nested
@Order(10)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RbacContextScenarios {
@Test
@Order(1010)
@Produces("RBAC Context")
void shouldFetchRbacContext() {
new FetchRbacContext(scenarioTest)
.given("subjectName", "superuser-fran@hostsharing.net")
.given("assumedRoles", "rbactest.package#xxx00:ADMIN;rbactest.package#yyy00:ADMIN")
.given("expectedToBeGlobalAdmin", true)
.doRun()
.keep();
}
}
@Nested
@Order(20)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CurrentLoginUserScenarios {
@Test
@Order(2010)
@Produces("Current Login User")
void shouldFetchCurrentLoginUser() {
new CurrentLoginUser(scenarioTest)
.given("subjectName", "superuser-fran@hostsharing.net")
.given("personGivenName", "Fran")
.given("expectedToBeGlobalAdmin", true)
.doRun()
.keep();
}
}
@Nested
@Order(30)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CredentialScenarios {
@Test
@@ -0,0 +1,60 @@
package net.hostsharing.hsadminng.hs.accounts.scenarios;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import static io.restassured.http.ContentType.JSON;
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolve;
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.HttpStatus.OK;
public class CurrentLoginUser extends UseCase<CurrentLoginUser> {
public CurrentLoginUser(final ScenarioTest testSuite) {
super(testSuite);
introduction("Fetches data about the current login user.");
}
@Override
protected HttpResponse run() {
obtain("Person: %{personGivenName}", () ->
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{personGivenName}"))
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
);
return obtain(
"Current Login User", () ->
httpGet(
"/api/hs/accounts/current", req -> req
.header("Authorization", resolve("Bearer %{subjectName}", DROP_COMMENTS))
)
.expecting(OK).expecting(JSON).expectObject()
.extractValue("subject.name", "returnedSubjectName")
.extractValue("person.givenName", "returnedGivenName")
.extractValue("globalAdmin", "returnedGlobalAdmin")
).expecting(OK).expecting(JSON);
}
@Override
@SneakyThrows
protected void verify(final UseCase<CurrentLoginUser>.HttpResponse response) {
assertThat(resolve("%{returnedSubjectName}", DROP_COMMENTS))
.isEqualTo(resolve("%{subjectName}", DROP_COMMENTS));
assertThat(resolve("%{returnedGivenName}", DROP_COMMENTS))
.isEqualTo(resolve("%{personGivenName}", DROP_COMMENTS));
assertThat(resolve("%{returnedGlobalAdmin}", DROP_COMMENTS))
.isEqualTo(resolve("%{expectedToBeGlobalAdmin}", DROP_COMMENTS));
super.verify(response);
}
}
@@ -0,0 +1,58 @@
package net.hostsharing.hsadminng.hs.accounts.scenarios;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import java.util.List;
import static io.restassured.http.ContentType.JSON;
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolve;
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolveJsonArray;
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.HttpStatus.OK;
public class FetchRbacContext extends UseCase<FetchRbacContext> {
public FetchRbacContext(final ScenarioTest testSuite) {
super(testSuite);
introduction("Fetches the RBAC context for the login user / current subject.");
}
@Override
protected HttpResponse run() {
return obtain(
"RBAC Context", () ->
httpGet(
"/api/rbac/context", req -> req
.header("Authorization", resolve("Bearer %{subjectName}", DROP_COMMENTS))
.header("assumed-roles", resolve("%{assumedRoles}", DROP_COMMENTS))
)
.expecting(OK).expecting(JSON).expectObject()
.extractValue("subject.name", "returnedSubjectName")
.extractValue("assumedRoles", "returnedAssumedRoles")
.extractValue("globalAdmin", "returnedGlobalAdmin")
).expecting(OK).expecting(JSON);
}
@Override
@SneakyThrows
protected void verify(final UseCase<FetchRbacContext>.HttpResponse response) {
// HOWTO: assert in UseCase.verify()
assertThat(resolve("%{returnedSubjectName}", DROP_COMMENTS))
.isEqualTo(resolve("%{subjectName}", DROP_COMMENTS));
assertThat(resolveJsonArray("%{returnedAssumedRoles}")
.stream().map(m -> m.get("roleName")).toList())
.isEqualTo(List.of(resolve("%{assumedRoles}", DROP_COMMENTS).split(";")));
assertThat(resolve("%{returnedGlobalAdmin}", DROP_COMMENTS))
.isEqualTo(resolve("%{expectedToBeGlobalAdmin}", DROP_COMMENTS));
super.verify(response);
}
}
@@ -78,6 +78,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
@MockitoBean
HttpServletRequest request;
@Nested
class CreateDebitor {
@@ -242,9 +243,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// then
allTheseDebitorsAreReturned(
result,
"debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)",
"debitor(D-1000212: rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='DEBITOR', holder='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.'), sec)",
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
"debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)",
"debitor(D-1000212: rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=DEBITOR, holder='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.'), sec)",
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
}
@ParameterizedTest
@@ -293,7 +294,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// then
assertThat(result).map(Object::toString).contains(
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
}
}
@@ -310,7 +311,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// then
exactlyTheseDebitorsAreReturned(result,
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
}
}
@@ -326,7 +327,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final var result = debitorRepo.findDebitorsByOptionalNameLike("third contact");
// then
exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
}
}
@@ -413,7 +413,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
final var newPartnerPersonUuid = givenPartner.getPartnerRel().getHolder().getUuid();
assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(newPartnerPersonUuid, EX_PARTNER, null, null, null))
.map(HsOfficeRelation::toShortString)
.contains("rel(anchor='NP Winkler, Paul', type='EX_PARTNER', holder='UF Erben Bessler')");
.contains("rel(anchor='NP Winkler, Paul', type=EX_PARTNER, holder='UF Erben Bessler')");
}
@Test
@@ -144,6 +144,7 @@ class HsOfficePersonEntityUnitTest {
assertThat(actualDisplay).isEqualTo("person(salutation='Herr', title='Prof. Dr.', familyName='some family name', givenName='some given name')");
}
@Test
void toStringWithSalutationAndWithoutTitleSkipsTitle() {
final var givenPersonEntity = HsOfficePersonRbacEntity.builder()
@@ -258,6 +258,23 @@ class HsOfficePersonRbacRepositoryIntegrationTest extends ContextBasedTestWithCl
}
}
@Test
public void findPersonsrepresentedByPersonWithUuid() {
// given
context("superuser-alex@hostsharing.net");
final var personUuid = personRbacRepo.findPersonByOptionalNameLike("Fouler").getFirst().getUuid();
// when
@SuppressWarnings("unchecked") final List<HsOfficePersonRbacEntity> representedPersons = personRbacRepo.findPersonsrepresentedByPersonWithUuid(personUuid);
// then
assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder(
"person(personType=NP, familyName='Fouler', givenName='Ellie')",
"person(personType=LP, tradeName='Fourth eG')"
);
}
@Test
public void auditJournalLogIsAvailable() {
// given
@@ -265,7 +282,7 @@ class HsOfficePersonRbacRepositoryIntegrationTest extends ContextBasedTestWithCl
select currentTask, targetTable, targetOp, targetdelta->>'tradename', targetdelta->>'lastname'
from base.tx_journal_v
where targettable = 'hs_office.person';
""");
""");
// when
@SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
@@ -62,10 +62,10 @@ class HsOfficeRealRelationRepositoryIntegrationTest extends ContextBasedTestWith
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
exactlyTheseRelationsAreReturned(
result,
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='NP Smith, Peter', type='DEBITOR', holder='NP Smith, Peter', contact='third contact')",
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')"
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='NP Smith, Peter', type=DEBITOR, holder='NP Smith, Peter', contact='third contact')",
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')"
);
}
@@ -81,7 +81,7 @@ class HsOfficeRealRelationRepositoryIntegrationTest extends ContextBasedTestWith
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
exactlyTheseRelationsAreReturned(
result,
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')"
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')"
);
}
}
@@ -126,7 +126,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
final var stored = relationRbacRepo.findByUuid(result.returnedValue().getUuid());
assertThat(stored).isNotEmpty().map(HsOfficeRelation::toString).get()
.isEqualTo(
"rel(anchor='UF Erben Bessler', type='SUBSCRIBER', mark='operations-announce', holder='NP Winkler, Paul', contact='fourth contact')");
"rel(anchor='UF Erben Bessler', type=SUBSCRIBER, mark='operations-announce', holder='NP Winkler, Paul', contact='fourth contact')");
}
@Test
@@ -213,9 +213,9 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
// then
allTheseRelationsAreReturned(
result,
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')");
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')");
}
@Test
@@ -237,10 +237,10 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
// then:
exactlyTheseRelationsAreReturned(
result,
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')",
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='NP Smith, Peter', type='DEBITOR', holder='NP Smith, Peter', contact='third contact')");
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')",
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
"rel(anchor='NP Smith, Peter', type=DEBITOR, holder='NP Smith, Peter', contact='third contact')");
}
}
@@ -383,7 +383,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
context.define("superuser-alex@hostsharing.net");
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
.matches(mandate -> {
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)");
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)");
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z - patched");
assertThat(mandate.getValidFrom()).isEqualTo("2020-06-05");
@@ -424,7 +424,8 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
// finally, the sepaMandate is actually updated
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
.matches(mandate -> {
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)");
assertThat(mandate.getDebitor().toString())
.isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)");
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z");
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)");
@@ -1,9 +1,11 @@
package net.hostsharing.hsadminng.hs.scenarios;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.jetbrains.annotations.NotNull;
@@ -17,6 +19,7 @@ import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
@@ -159,7 +162,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
assertThat(knowVariables().containsKey(declaredAlias))
.as("@Producer method " + currentTestMethod.getName() +
" did declare but not produce \"" + declaredAlias + "\"")
.isTrue() );
.isTrue());
}
});
}
@@ -203,6 +206,14 @@ public abstract class ScenarioTest extends ContextBasedTest {
return resolved;
}
@SneakyThrows
public static List<Map<String, Object>> resolveJsonArray(final String text) {
return new ObjectMapper().readValue(
resolve(text, DROP_COMMENTS),
new TypeReference<>() {}
);
}
public static Object resolveTyped(final String resolvableText) {
final var resolved = resolve(resolvableText, DROP_COMMENTS);
try {
@@ -155,18 +155,26 @@ public abstract class UseCase<T extends UseCase<?>> {
}
@SneakyThrows
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
final var request = HttpRequest.newBuilder()
public final HttpResponse httpGet(
final String uriPathWithPlaceholder,
final Function<HttpRequest.Builder, HttpRequest.Builder> requestCustomizer) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholder, DROP_COMMENTS);
final var requestBuilder = HttpRequest.newBuilder()
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Authorization", "Bearer " + ScenarioTest.RUN_AS_USER)
.timeout(seconds(HTTP_TIMEOUT_SECONDS))
.build();
.timeout(seconds(HTTP_TIMEOUT_SECONDS));
final var customizedRequestBuilder = requestCustomizer.apply(requestBuilder);
final var request = customizedRequestBuilder.build();
final var response = client.send(request, BodyHandlers.ofString());
return new HttpResponse(HttpMethod.GET, uriPath, null, response);
}
@SneakyThrows
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
return httpGet(uriPathWithPlaceholders,
req -> req.header("Authorization", "Bearer " + ScenarioTest.RUN_AS_USER));
}
@SneakyThrows
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
@@ -68,7 +68,7 @@ class ContextIntegrationTests {
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
assertThat(context.fetchAssumedRoles()).isEmpty();
assertThat(context.fetchAssumedRolesNames()).isEmpty();
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
.containsExactly(context.fetchCurrentSubjectUuid());
@@ -90,7 +90,7 @@ class ContextIntegrationTests {
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
assertThat(context.fetchAssumedRoles()).isEqualTo(Array.of("rbactest.package#yyy00:ADMIN"));
assertThat(context.fetchAssumedRolesNames()).isEqualTo(Array.of("rbactest.package#yyy00:ADMIN"));
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
.containsExactly(context.fetchCurrentSubjectOrAssumedRolesUuids());
@@ -133,7 +133,7 @@ class ContextIntegrationTests {
assertThat(currentSubject).isEqualTo("superuser-alex@hostsharing.net");
// then
assertThat(context.fetchAssumedRoles())
assertThat(context.fetchAssumedRolesNames())
.isEqualTo(Array.of("rbactest.customer#xxx:OWNER", "rbactest.customer#yyy:OWNER"));
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids()).hasSize(2);
}
@@ -0,0 +1,126 @@
package net.hostsharing.hsadminng.rbac.context;
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.role.RbacRoleEntity;
import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.role.RbacRoleType;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import org.junit.jupiter.api.BeforeEach;
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.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.SynchronizationType;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(RbacContextController.class)
@Import({ StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class })
@ActiveProfiles("test")
class RbacContextControllerRestTest {
private static final String GIVEN_SUBJECT_NAME = "superuser-alex@hostsharing.net";
private static final boolean GIVEN_GLOBAL_ADMIN = true;
@Autowired
MockMvc mockMvc;
@MockitoBean
Context contextMock;
@MockitoBean
RbacRoleRepository rbacRoleRepository;
@MockitoBean
RbacSubjectRepository rbacSubjectRepository;
@MockitoBean
EntityManagerWrapper em;
@MockitoBean
EntityManagerFactory emf;
@BeforeEach
void init() {
when(emf.createEntityManager()).thenReturn(em);
when(emf.createEntityManager(any(Map.class))).thenReturn(em);
when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em);
when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em);
// current subject uuid mock
final var mockUuid = UUID.randomUUID();
when(contextMock.fetchCurrentSubjectUuid()).thenReturn(mockUuid);
// find by uuid mock
final var mockSubject = new RbacSubjectEntity();
mockSubject.setUuid(mockUuid);
mockSubject.setName(GIVEN_SUBJECT_NAME);
when(rbacSubjectRepository.findByUuid(mockUuid)).thenReturn(mockSubject);
}
@Test
void apiContextWillReturnCurrentContext() throws Exception {
// given
final var rolesToAssume = "rbactest.package#xxx00:OWNER;rbactest.package#yyy00:OWNER";
when(contextMock.isGlobalAdmin()).thenReturn(GIVEN_GLOBAL_ADMIN);
when(rbacRoleRepository.fetchAssumedRoles()).thenReturn(
Arrays.stream(rolesToAssume.split(";"))
.map(RbacRoleDescriptor::fromRoleName)
.map(roleDesc -> new RbacRoleEntity(
UUID.randomUUID(), UUID.randomUUID(),
roleDesc.tableName, roleDesc.objectIdName, roleDesc.roleType,
roleDesc.roleName))
.toList()
);
// when
mockMvc.perform(MockMvcRequestBuilders
.get("/api/rbac/context")
.header("Authorization", "Bearer " + GIVEN_SUBJECT_NAME)
.header("assumed-roles", rolesToAssume)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
// then
.andExpect(status().isOk())
.andExpect(jsonPath("$.subject.name", is(GIVEN_SUBJECT_NAME)))
.andExpect(jsonPath("$.globalAdmin", is(GIVEN_GLOBAL_ADMIN)))
.andExpect(jsonPath("$.assumedRoles", hasSize(2)))
.andExpect(jsonPath("$.assumedRoles[0].roleName", is("rbactest.package#xxx00:OWNER")))
.andExpect(jsonPath("$.assumedRoles[1].roleName", is("rbactest.package#yyy00:OWNER")));
}
record RbacRoleDescriptor(String roleName, String tableName, String objectIdName, RbacRoleType roleType) {
private static RbacRoleDescriptor fromRoleName(final String roleName) {
final var tablePlus = roleName.split("#");
final var tableName = tablePlus[0];
final var objectId = tablePlus[1].split(":")[0];
final var roleType = RbacRoleType.valueOf(tablePlus[1].split(":")[1]);
return new RbacRoleDescriptor(roleName, tableName, objectId, roleType);
}
}
}
@@ -100,6 +100,6 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu
final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office.coopassettx WHERE reference='ref 1000101-1'").getSingleResult();
final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS));
RbacGrantsDiagramService.writeToFile(join(";", context.fetchAssumedRoles()), graph, "doc/all-grants.md");
RbacGrantsDiagramService.writeToFile(join(";", context.fetchAssumedRolesNames()), graph, "doc/all-grants.md");
}
}
@@ -177,6 +177,31 @@ class RbacRoleRepositoryIntegrationTest {
}
}
@Nested
class FetchAssumedRoles {
@Test
void someSubject_withoutAssumedRole_fetchesNoAssumedRoles() {
context.define("customer-admin@xxx.example.com");
final var result = rbacRoleRepository.fetchAssumedRoles();
assertThat(result).isNotNull().hasSize(0);
}
@Test
void someSubject_withAssumedRoles_fetchesAssumedRoles() {
context.define("customer-admin@xxx.example.com",
"rbactest.package#xxx00:OWNER;rbactest.package#xxx01:OWNER;rbactest.package#xxx02:OWNER");
final var result = rbacRoleRepository.fetchAssumedRoles();
assertThat(result).isNotNull().hasSize(3)
.extracting(RbacRoleEntity::getRoleName)
.contains("rbactest.package#xxx00:OWNER", "rbactest.package#xxx01:OWNER", "rbactest.package#xxx02:OWNER");
}
}
void exactlyTheseRbacRolesAreReturned(final List<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
assertThat(actualResult)
.extracting(RbacRoleEntity::getRoleName)
@@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.rbac.test;
import net.hostsharing.hsadminng.persistence.ImmutableBaseEntity;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.grant.RbacGrantEntity;
@@ -52,7 +53,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
@Autowired
JpaAttempt jpaAttempt;
private TreeMap<UUID, Class<? extends BaseEntity>> entitiesToCleanup = new TreeMap<>();
private TreeMap<UUID, Class<? extends ImmutableBaseEntity>> entitiesToCleanup = new TreeMap<>();
private static Long latestIntialTestDataSerialId;
private static boolean countersInitialized = false;
@@ -67,19 +68,19 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
private TestInfo testInfo;
public <T extends BaseEntity> T refresh(final T entity) {
public <T extends ImmutableBaseEntity> T refresh(final T entity) {
final var merged = em.merge(entity);
em.refresh(merged);
return merged;
}
public UUID toCleanup(final Class<? extends BaseEntity> entityClass, final UUID uuidToCleanup) {
public UUID toCleanup(final Class<? extends ImmutableBaseEntity> entityClass, final UUID uuidToCleanup) {
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup + ")");
entitiesToCleanup.put(uuidToCleanup, entityClass);
return uuidToCleanup;
}
public <E extends BaseEntity> E toCleanup(final E entity) {
public <E extends ImmutableBaseEntity> E toCleanup(final E entity) {
out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid());
if ( entity.getUuid() == null ) {
throw new IllegalArgumentException("only persisted entities with valid uuid allowed");