Story#5617: amend account module to Keycloak primary (#213)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/213
This commit is contained in:
+253
@@ -0,0 +1,253 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.accounts.HsAccountEntity.HsAccountEntityBuilder;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
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 org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||
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.hamcrest.Matchers.equalTo;
|
||||
|
||||
@Tag("generalIntegrationTest")
|
||||
@Transactional
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
)
|
||||
@ActiveProfiles("fake-jwt")
|
||||
// too complex database interaction for just a RestTest, thus a fully integrated test
|
||||
class HsAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||
|
||||
@LocalServerPort
|
||||
Integer port;
|
||||
|
||||
@Autowired
|
||||
Context context;
|
||||
|
||||
@Autowired
|
||||
RbacSubjectRepository rbacSubjectRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficePersonRealRepository realPersonRepo;
|
||||
|
||||
@Autowired
|
||||
HsAccountRepository accountRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetCurrentUser {
|
||||
|
||||
@Test
|
||||
void shouldFetchCurrentLoginUser() throws Exception {
|
||||
// given
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/accounts/current")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("subject.name", equalTo("superuser-alex@hostsharing.net"))
|
||||
.body("globalAdmin", equalTo(true));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetAccountByUuid {
|
||||
|
||||
@Test
|
||||
void shouldGetAccountByUuid() {
|
||||
// given
|
||||
val legalPerson = givenLegalPerson("selfregistered-user-drew@hostsharing.org");
|
||||
val accountEntity = givenNewAccount("selfregistered-user-drew@hostsharing.org",
|
||||
"test-subject1", legalPerson, builder -> {
|
||||
});
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer(accountEntity.getSubject().getName()))
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/accounts/accounts/" + accountEntity.getUuid())
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("$", lenientlyEquals("""
|
||||
{
|
||||
"person": {
|
||||
"personType": "LEGAL_PERSON",
|
||||
"tradeName": "Test Company",
|
||||
"salutation": null,
|
||||
"title": null,
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
"subjectName": "test-subject1",
|
||||
"globalUid": null,
|
||||
"globalGid": null
|
||||
}
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class PostNewAccount {
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingAccountForUnrepresentedPerson() {
|
||||
// given
|
||||
val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"person.uuid": "%s",
|
||||
"subjectName": "new-user",
|
||||
"globalUid": 30001,
|
||||
"globalGid": 40001
|
||||
}
|
||||
""".formatted(testPerson.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/accounts/accounts")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("wird von der eingeloggten Person nicht repräsentiert"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingAccountForNonNaturalPerson() {
|
||||
// given
|
||||
val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"person.uuid": "%s",
|
||||
"subjectName": "new-user",
|
||||
"globalUid": 30001,
|
||||
"globalGid": 40001
|
||||
}
|
||||
""".formatted(firstGmbHPerson.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/accounts/accounts")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(400)
|
||||
.contentType("application/json")
|
||||
.body("message",
|
||||
containsString("Nur natürliche Personen sind erlaubt, aber ${personUuid} ist LEGAL_PERSON"
|
||||
.replace("${personUuid}", firstGmbHPerson.getUuid().toString())));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
// TODO.spec Task#5637: add @Nested class DeleteAccount and tests for delete, when we have a spec
|
||||
|
||||
// Helper methods
|
||||
|
||||
private HsOfficePersonRealEntity givenLegalPerson(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(LEGAL_PERSON)
|
||||
.tradeName("Test Company")
|
||||
.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenNaturalPerson(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(NATURAL_PERSON)
|
||||
.familyName("Test")
|
||||
.givenName("User")
|
||||
.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenPersonWithUuid(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(NATURAL_PERSON)
|
||||
.familyName("Test")
|
||||
.givenName("Person")
|
||||
.build()));
|
||||
}).returnedValue();
|
||||
}
|
||||
|
||||
private HsAccountEntity givenNewAccount(
|
||||
final String executingSubjectName,
|
||||
final String newSubjectName, final HsOfficePersonRealEntity person,
|
||||
final Consumer<HsAccountEntityBuilder> modifier
|
||||
) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
|
||||
// only RbacSubject entities can be created
|
||||
val subject = rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name(newSubjectName)
|
||||
.build());
|
||||
|
||||
context.define(subject.getName());
|
||||
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
||||
val accountBuilder = HsAccountEntity.builder()
|
||||
.person(attachedPerson)
|
||||
.subject(em.find(RealSubjectEntity.class, subject.getUuid()));
|
||||
modifier.accept(accountBuilder);
|
||||
return toCleanup(accountRepo.save(accountBuilder.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HsAccountEntityUnitTest {
|
||||
|
||||
static final HsAccountEntity GIVEN_ACCOUNT_ENTITY = HsAccountEntity.builder()
|
||||
.uuid(UUID.fromString("11111111-1111-1111-1111-111111111111"))
|
||||
.subject(
|
||||
RealSubjectEntity.builder().uuid(UUID.randomUUID()).name("test-subject").build())
|
||||
.person(
|
||||
HsOfficePersonRealEntity.builder()
|
||||
.personType(HsOfficePersonType.NATURAL_PERSON)
|
||||
.familyName("Miller")
|
||||
.givenName("John")
|
||||
.build()
|
||||
)
|
||||
.globalUid(10001)
|
||||
.globalUid(20002)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void toShortStringContainsJustTheSubjectName() {
|
||||
assertThat(GIVEN_ACCOUNT_ENTITY.toShortString()).isEqualTo("test-subject");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContainsJustTheSubjectNam() {
|
||||
assertThat(GIVEN_ACCOUNT_ENTITY.toString()).isEqualTo("account(test-subject)");
|
||||
}
|
||||
}
|
||||
+34
-141
@@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.hibernate.TransientObjectException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -32,12 +31,11 @@ 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 HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
class HsAccountRepositoryIntegrationTest 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";
|
||||
@@ -58,10 +56,7 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
private HsOfficePersonRealRepository personRepo;
|
||||
|
||||
@Autowired
|
||||
private HsProfileRepository profileRepository;
|
||||
|
||||
@Autowired
|
||||
private HsProfileScopeRealRepository scopeRealRepo;
|
||||
private HsAccountRepository accountRepository;
|
||||
|
||||
// fetched UUIDs from test-data
|
||||
private RealSubjectEntity alexSubject;
|
||||
@@ -82,7 +77,7 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
@Test
|
||||
public void historizationIsAvailable() {
|
||||
// given
|
||||
final String nativeQuerySql = "select * from hs_accounts.profile_hv";
|
||||
final String nativeQuerySql = "select * from hs_accounts.account_hv";
|
||||
|
||||
// when
|
||||
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
||||
@@ -91,7 +86,7 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
|
||||
// then
|
||||
assertThat(rowsBefore)
|
||||
.as("hs_accounts.profile_hv only contain no rows for a timestamp before test data creation")
|
||||
.as("hs_accounts.account_hv only contain no rows for a timestamp before test data creation")
|
||||
.hasSize(0);
|
||||
|
||||
// and when
|
||||
@@ -101,12 +96,12 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
|
||||
// then
|
||||
assertThat(rowsAfter)
|
||||
.as("hs_accounts.profile_hv should now contain the test-data rows for the current timestamp")
|
||||
.as("hs_accounts.account_hv should now contain the test-data rows for the current timestamp")
|
||||
.hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void representativeShouldFindOwnAndRepresentedProfileByCurrentSubject() {
|
||||
void representativeShouldFindOwnAndRepresentedAccountByCurrentSubject() {
|
||||
// given
|
||||
final var firstGmbHPerson = givenPerson("First GmbH");
|
||||
givenRelation(REPRESENTATIVE)
|
||||
@@ -114,163 +109,76 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
.withHolder(drewPerson)
|
||||
.withContact("some test contact")
|
||||
.inDatabase();
|
||||
givenProfile()
|
||||
givenAccount()
|
||||
.forSubject("first-gmbh")
|
||||
.forPerson(firstGmbHPerson)
|
||||
.withEMailAddress("first-gmbh@example.com")
|
||||
.inDatabase();
|
||||
|
||||
// when
|
||||
final var foundProfile = attempt(
|
||||
final var foundAccount = attempt(
|
||||
em, () -> {
|
||||
context(USER_DREW_SUBJECT_NAME);
|
||||
return profileRepository.findByCurrentSubject();
|
||||
return accountRepository.findByCurrentSubject();
|
||||
})
|
||||
.assertNotNull().returnedValue();
|
||||
|
||||
// then
|
||||
assertThat(foundProfile).hasSize(2)
|
||||
.map(HsProfileEntity::getEmailAddress)
|
||||
assertThat(foundAccount).hasSize(2)
|
||||
.map(e -> e.getSubject().getName())
|
||||
.containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalAdminShouldFindOnlyOwnProfileByCurrentSubject() {
|
||||
void globalAdminShouldFindOnlyOwnAccountByCurrentSubject() {
|
||||
|
||||
// when
|
||||
final var foundProfile = attempt(
|
||||
final var foundAccount = attempt(
|
||||
em, () -> {
|
||||
context(SUPERUSER_FRAN_SUBJECT_NAME);
|
||||
return profileRepository.findByCurrentSubject();
|
||||
return accountRepository.findByCurrentSubject();
|
||||
})
|
||||
.assertNotNull().returnedValue();
|
||||
|
||||
// then
|
||||
assertThat(foundProfile).hasSize(1)
|
||||
.map(HsProfileEntity::getEmailAddress)
|
||||
assertThat(foundAccount).hasSize(1)
|
||||
.map(e -> e.getSubject().getName())
|
||||
.containsExactlyInAnyOrder("fran@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByUuidUsingTestData() {
|
||||
// when
|
||||
final var foundEntityOptional = profileRepository.findByUuid(alexSubject.getUuid());
|
||||
final var foundEntityOptional = accountRepository.findByUuid(alexSubject.getUuid());
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isPresent()
|
||||
.map(HsProfileEntity::getEmailAddress).contains("alex@example.com");
|
||||
.map(e -> e.getSubject().getName())
|
||||
.contains("alex@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveProfileWithExistingScope() {
|
||||
void shouldSaveAccount() {
|
||||
// given
|
||||
final var existingScope = scopeRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||
.orElseThrow();
|
||||
final var newProfile = HsProfileEntity.builder()
|
||||
final var newAccount = HsAccountEntity.builder()
|
||||
.subject(testUserSubject)
|
||||
.person(testUserPerson)
|
||||
.active(true)
|
||||
.emailAddress("test-user@example.com")
|
||||
.globalUid(2011)
|
||||
.globalGid(2011)
|
||||
.scopes(mutableSetOf(existingScope))
|
||||
.build();
|
||||
|
||||
// when
|
||||
toCleanup(profileRepository.save(newProfile));
|
||||
toCleanup(accountRepository.save(newAccount));
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// then
|
||||
final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid());
|
||||
final var foundEntityOptional = accountRepository.findByUuid(testUserSubject.getUuid());
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
final var foundEntity = foundEntityOptional.get();
|
||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("test-user@example.com");
|
||||
assertThat(foundEntity.isActive()).isTrue();
|
||||
assertThat(foundEntity.getVersion()).isEqualTo(0); // Initial version
|
||||
assertThat(foundEntity.getGlobalUid()).isEqualTo(2011);
|
||||
|
||||
assertThat(foundEntity.getScopes()).hasSize(1)
|
||||
.map(HsProfileScopeRealEntity::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotSaveProfileWithNewScope() {
|
||||
// given
|
||||
final var newScope = HsProfileScopeRealEntity.builder()
|
||||
.type("MATRIX")
|
||||
.qualifier("forbidden")
|
||||
.build();
|
||||
final var newProfile = HsProfileEntity.builder()
|
||||
.subject(drewSubject)
|
||||
.active(true)
|
||||
.emailAddress("drew.new@example.com")
|
||||
.globalUid(2001)
|
||||
.globalGid(2001)
|
||||
.scopes(mutableSetOf(newScope))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var exception = catchThrowable(() -> {
|
||||
profileRepository.save(newProfile);
|
||||
em.flush();
|
||||
});
|
||||
|
||||
// then
|
||||
assertThat(exception).isNotNull().hasCauseInstanceOf(TransientObjectException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveNewProfileWithoutScope() {
|
||||
// given
|
||||
final var newProfile = HsProfileEntity.builder()
|
||||
.subject(testUserSubject)
|
||||
.person(testUserPerson)
|
||||
.active(true)
|
||||
.emailAddress("test.user.new@example.com")
|
||||
.globalUid(20002)
|
||||
.globalGid(2002)
|
||||
.build();
|
||||
|
||||
// when
|
||||
profileRepository.save(newProfile);
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// then
|
||||
final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid());
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
final var foundEntity = foundEntityOptional.get();
|
||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com");
|
||||
assertThat(foundEntity.isActive()).isTrue();
|
||||
assertThat(foundEntity.getGlobalUid()).isEqualTo(20002);
|
||||
assertThat(foundEntity.getGlobalGid()).isEqualTo(2002);
|
||||
assertThat(foundEntity.getScopes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateExistingProfile() {
|
||||
// given
|
||||
final var entityToUpdate = profileRepository.findByUuid(alexSubject.getUuid()).orElseThrow();
|
||||
final var initialVersion = entityToUpdate.getVersion();
|
||||
|
||||
// when
|
||||
entityToUpdate.setActive(false);
|
||||
entityToUpdate.setEmailAddress("updated.user1@example.com");
|
||||
final var savedEntity = profileRepository.save(entityToUpdate);
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// then
|
||||
assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion);
|
||||
final var updatedEntityOptional = profileRepository.findByUuid(alexSubject.getUuid());
|
||||
assertThat(updatedEntityOptional).isPresent();
|
||||
final var updatedEntity = updatedEntityOptional.get();
|
||||
assertThat(updatedEntity.isActive()).isFalse();
|
||||
assertThat(updatedEntity.getEmailAddress()).isEqualTo("updated.user1@example.com");
|
||||
}
|
||||
|
||||
|
||||
private RealSubjectEntity fetchSubjectByName(final String name) {
|
||||
final String jpql = "SELECT s FROM RealSubjectEntity s WHERE s.name = :name";
|
||||
final Query query = em.createQuery(jpql, RealSubjectEntity.class);
|
||||
@@ -315,8 +223,8 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
return new RelationBuilder(relationType);
|
||||
}
|
||||
|
||||
private ProfileBuilder givenProfile() {
|
||||
return new ProfileBuilder();
|
||||
private AccountBuilder givenAccount() {
|
||||
return new AccountBuilder();
|
||||
}
|
||||
|
||||
private class RelationBuilder {
|
||||
@@ -361,12 +269,11 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
private class ProfileBuilder {
|
||||
private class AccountBuilder {
|
||||
private RealSubjectEntity subject;
|
||||
private HsOfficePersonRealEntity person;
|
||||
private String emailAddress;
|
||||
|
||||
public ProfileBuilder forSubject(String subjectName) {
|
||||
public AccountBuilder forSubject(String subjectName) {
|
||||
// only the RbacSubject can be created
|
||||
val rbacSubject = toCleanup(rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name(subjectName)
|
||||
@@ -376,36 +283,22 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileBuilder forPerson(HsOfficePersonRealEntity person) {
|
||||
public AccountBuilder forPerson(HsOfficePersonRealEntity person) {
|
||||
this.person = person;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileBuilder withEMailAddress(String emailAddress) {
|
||||
this.emailAddress = emailAddress;
|
||||
final var profile = HsProfileEntity.builder()
|
||||
public HsAccountEntity inDatabase() {
|
||||
|
||||
final var account = HsAccountEntity.builder()
|
||||
.uuid(subject.getUuid())
|
||||
.subject(subject)
|
||||
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
||||
.emailAddress(emailAddress)
|
||||
.active(true)
|
||||
.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public HsProfileEntity inDatabase() {
|
||||
|
||||
final var profile = HsProfileEntity.builder()
|
||||
.uuid(subject.getUuid())
|
||||
.subject(subject)
|
||||
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
||||
.emailAddress(emailAddress)
|
||||
.active(true)
|
||||
.build();
|
||||
em.persist(profile);
|
||||
toCleanup(profile);
|
||||
em.persist(account);
|
||||
toCleanup(account);
|
||||
em.flush();
|
||||
return profile;
|
||||
return account;
|
||||
}
|
||||
}
|
||||
}
|
||||
-488
@@ -1,488 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.accounts.HsProfileEntity.HsProfileEntityBuilder;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
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 org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||
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.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
@Tag("generalIntegrationTest")
|
||||
@Transactional
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
|
||||
)
|
||||
@ActiveProfiles("fake-jwt")
|
||||
// too complex database interaction for just a RestTest, thus a fully integrated test
|
||||
class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||
|
||||
@LocalServerPort
|
||||
Integer port;
|
||||
|
||||
@Autowired
|
||||
Context context;
|
||||
|
||||
@Autowired
|
||||
RbacSubjectRepository rbacSubjectRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficePersonRealRepository realPersonRepo;
|
||||
|
||||
@Autowired
|
||||
HsProfileScopeRealRepository scopeRepo;
|
||||
|
||||
@Autowired
|
||||
HsProfileRepository profileRepo;
|
||||
|
||||
@Autowired
|
||||
HsProfileScopeRbacRepository scopeRbacRepo;
|
||||
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetCurrentUser {
|
||||
|
||||
@Test
|
||||
void shouldFetchCurrentLoginUser() throws Exception {
|
||||
// given
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/accounts/current")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("subject.name", equalTo("superuser-alex@hostsharing.net"))
|
||||
.body("globalAdmin", equalTo(true));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetProfileByUuid {
|
||||
|
||||
@Test
|
||||
void shouldFilterInvalidScopesRegardingNonNaturalPerson() {
|
||||
// given
|
||||
val legalPerson = givenLegalPerson("selfregistered-user-drew@hostsharing.org");
|
||||
val profileEntity = givenNewProfile("selfregistered-user-drew@hostsharing.org",
|
||||
"test-subject1", legalPerson, builder -> {
|
||||
builder.scopes(new HashSet<>(scopeRepo.findAll()));
|
||||
});
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer(profileEntity.getSubject().getName()))
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/accounts/profiles/" + profileEntity.getUuid())
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("$", lenientlyEquals("""
|
||||
{
|
||||
"person": {
|
||||
"personType": "LEGAL_PERSON",
|
||||
"tradeName": "Test Company",
|
||||
"salutation": null,
|
||||
"title": null,
|
||||
"givenName": null,
|
||||
"familyName": null
|
||||
},
|
||||
"nickname": "test-subject1",
|
||||
"totpSecrets": null,
|
||||
"phonePassword": null,
|
||||
"emailAddress": null,
|
||||
"smsNumber": null,
|
||||
"active": false,
|
||||
"globalUid": null,
|
||||
"globalGid": null,
|
||||
"scopes": [
|
||||
{
|
||||
"uuid": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "SSH",
|
||||
"qualifier": "external",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": true
|
||||
},
|
||||
{
|
||||
"uuid": "66666666-6666-6666-6666-666666666666",
|
||||
"type": "MASTODON",
|
||||
"qualifier": "external",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": true
|
||||
},
|
||||
{
|
||||
"uuid": "77777777-7777-7777-7777-777777777777",
|
||||
"type": "BBB",
|
||||
"qualifier": "external",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"""));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class PostNewProfile {
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingProfileForUnrepresentedPerson() {
|
||||
// given
|
||||
val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org");
|
||||
val publicScope = scopeRepo.findByTypeAndQualifier("SSH", "external").orElseThrow();
|
||||
assertThat(publicScope.isPublicAccess()).as("precondition failed").isTrue();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"person.uuid": "%s",
|
||||
"nickname": "new-user",
|
||||
"active": true,
|
||||
"globalUid": 30001,
|
||||
"globalGid": 40001,
|
||||
"scopes": [
|
||||
{
|
||||
"uuid" : "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(testPerson.getUuid(), publicScope.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/accounts/profiles")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("wird von der eingeloggten Person nicht repräsentiert"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingProfileWithPrivateScopeForNormalUser() {
|
||||
// given
|
||||
val drewPerson = realPersonRepo.findPersonByOptionalNameLike("Drew").getFirst();
|
||||
val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal")
|
||||
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||
val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"person.uuid": "%s",
|
||||
"nickname": "new-user",
|
||||
"active": true,
|
||||
"globalUid": 30001,
|
||||
"globalGid": 40001,
|
||||
"scopes": [
|
||||
{ "uuid" : "%s" },
|
||||
{ "uuid" : "%s" },
|
||||
{ "uuid" : "%s" }
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
drewPerson.getUuid(),
|
||||
publicExternalMatrixScope.getUuid(),
|
||||
privateInternalSshScope.getUuid(),
|
||||
privateInternalMatrixScope.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/accounts/profiles")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("Zugriff auf Geltungsbereich verweigert: 'MATRIX:internal', 'SSH:internal'"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingProfileWithNaturalPersonRequirementForNonNaturalPerson() {
|
||||
// given
|
||||
val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst();
|
||||
val hsadminProdScopeOnlyForNaturalPersons = scopeRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||
.map(HsProfileControllerAcceptanceTest::asNaturalPersonScope).orElseThrow();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"person.uuid": "%s",
|
||||
"nickname": "new-user",
|
||||
"active": true,
|
||||
"globalUid": 30001,
|
||||
"globalGid": 40001,
|
||||
"scopes": [
|
||||
{ "uuid" : "%s" }
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
firstGmbHPerson.getUuid(),
|
||||
hsadminProdScopeOnlyForNaturalPersons.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/accounts/profiles")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(400)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("Geltungsbereich verlangt eine natürliche Person: 'HSADMIN:prod'"));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class PatchProfile {
|
||||
|
||||
@Test
|
||||
void shouldRejectPatchingProfileWithPrivateScopeForNormalUser() {
|
||||
// given
|
||||
context.define("selfregistered-user-drew@hostsharing.org");
|
||||
val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||
.getSubject().getUuid();
|
||||
val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal")
|
||||
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||
val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"scopes": [
|
||||
{ "uuid" : "%s" },
|
||||
{ "uuid" : "%s" },
|
||||
{ "uuid" : "%s" }
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
privateInternalSshScope.getUuid(),
|
||||
publicExternalMatrixScope.getUuid(),
|
||||
privateInternalMatrixScope.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid)
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("Zugriff auf Geltungsbereich verweigert: 'MATRIX:internal', 'SSH:internal'"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPatchingProfileAndRemovingTheOwnHsadminProfile() {
|
||||
// given
|
||||
context.define("selfregistered-user-drew@hostsharing.org");
|
||||
val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||
.getSubject().getUuid();
|
||||
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"scopes": [
|
||||
{ "uuid" : "%s" }
|
||||
]
|
||||
}
|
||||
""".formatted(publicExternalMatrixScope.getUuid()))
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid)
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(400)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("die eigenen hsadmin-Profile dürfen nicht entfernt werden"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRejectActivatingProfileForNormalUser() {
|
||||
// given
|
||||
context.define("selfregistered-user-drew@hostsharing.org");
|
||||
val drewProfile = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow();
|
||||
val inactiveProfileUuid = createNewInactiveProfile(drewProfile.getPerson()).getSubject().getUuid();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"active": true
|
||||
}
|
||||
""")
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/accounts/profiles/" + inactiveProfileUuid)
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("Only global admins are allowed to activate an inactive profile"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private HsProfileEntity createNewInactiveProfile(final HsOfficePersonRealEntity person) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
// only RbacSubject entities can be created
|
||||
val rbacSubjectEntity = rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name("some-inactive-profile")
|
||||
.build());
|
||||
// but we need the RealSubjectEntity to be attached to the profile entity
|
||||
val realSubjectEntity = em.find(RealSubjectEntity.class, rbacSubjectEntity.getUuid());
|
||||
|
||||
val inactiveCopy = HsProfileEntity.builder()
|
||||
.person(person)
|
||||
.subject(realSubjectEntity)
|
||||
.active(false).build();
|
||||
em.persist(inactiveCopy);
|
||||
em.flush();
|
||||
return toCleanup(inactiveCopy);
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenLegalPerson(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(LEGAL_PERSON)
|
||||
.tradeName("Test Company")
|
||||
.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenNaturalPerson(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(NATURAL_PERSON)
|
||||
.familyName("Test")
|
||||
.givenName("User")
|
||||
.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenPersonWithUuid(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||
.personType(NATURAL_PERSON)
|
||||
.familyName("Test")
|
||||
.givenName("Person")
|
||||
.build()));
|
||||
}).returnedValue();
|
||||
}
|
||||
|
||||
private static HsProfileScopeRealEntity asNaturalPersonScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||
assertThat(scope.isOnlyForNaturalPersons()).as("precondition failed").isTrue();
|
||||
return scope;
|
||||
}
|
||||
|
||||
private static HsProfileScopeRealEntity asPrivateScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||
assertThat(scope.isPublicAccess()).as("precondition failed").isFalse();
|
||||
return scope;
|
||||
}
|
||||
|
||||
private static HsProfileScopeRealEntity asPublicScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||
assertThat(scope.isPublicAccess()).as("precondition failed").isTrue();
|
||||
return scope;
|
||||
}
|
||||
|
||||
private HsProfileEntity givenNewProfile(
|
||||
final String executingSubjectName,
|
||||
final String newSubjectName, final HsOfficePersonRealEntity person,
|
||||
final Consumer<HsProfileEntityBuilder> modifier
|
||||
) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
|
||||
// only RbacSubject entities can be created
|
||||
val subject = rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name(newSubjectName)
|
||||
.build());
|
||||
|
||||
context.define(subject.getName());
|
||||
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
||||
val profileBuilder = HsProfileEntity.builder()
|
||||
.person(attachedPerson)
|
||||
.subject(em.find(RealSubjectEntity.class, subject.getUuid()))
|
||||
.scopes(Set.of());
|
||||
modifier.accept(profileBuilder);
|
||||
return toCleanup(profileRepo.save(profileBuilder.build()));
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
}
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
|
||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource;
|
||||
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@TestInstance(PER_CLASS)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HsProfileEntityPatcherUnitTest extends PatchUnitTestBase<
|
||||
ProfilePatchResource,
|
||||
HsProfileEntity
|
||||
> {
|
||||
|
||||
private static final UUID INITIAL_PROFILE_UUID = UUID.randomUUID();
|
||||
|
||||
private static final Boolean INITIAL_ACTIVE = true;
|
||||
private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com";
|
||||
private static final List<String> INITIAL_TOTP_SECRETS = List.of("initial_2fa");
|
||||
private static final String INITIAL_SMS_NUMBER = "initial_sms";
|
||||
private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw";
|
||||
|
||||
private static final Boolean PATCHED_ACTIVE = false;
|
||||
private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com";
|
||||
private static final List<String> PATCHED_TOTP_SECRETS = List.of("patched_2fa");
|
||||
private static final String PATCHED_SMS_NUMBER = "patched_sms";
|
||||
private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw";
|
||||
|
||||
// Scopes
|
||||
private static final UUID SCOPE_UUID_1 = UUID.randomUUID();
|
||||
private static final UUID SCOPE_UUID_2 = UUID.randomUUID();
|
||||
private static final UUID SCOPE_UUID_3 = UUID.randomUUID();
|
||||
|
||||
private final HsProfileScopeRealEntity initialScopeEntity1 = HsProfileScopeRealEntity.builder()
|
||||
.uuid(SCOPE_UUID_1)
|
||||
.type("HSADMIN")
|
||||
.qualifier("prod")
|
||||
.build();
|
||||
private final HsProfileScopeRealEntity initialScopeEntity2 = HsProfileScopeRealEntity.builder()
|
||||
.uuid(SCOPE_UUID_2)
|
||||
.type("SSH")
|
||||
.qualifier("dev")
|
||||
.build();
|
||||
|
||||
// This is what em.find should return for SCOPE_UUID_3
|
||||
private final HsProfileScopeRealEntity newScopeEntity3 = HsProfileScopeRealEntity.builder()
|
||||
.uuid(SCOPE_UUID_3)
|
||||
.type("HSADMIN")
|
||||
.qualifier("test")
|
||||
.build();
|
||||
|
||||
private final Set<HsProfileScopeRealEntity> initialScopeEntities = Set.of(initialScopeEntity1, initialScopeEntity2);
|
||||
private List<ScopeResource> patchedScopeResources;
|
||||
private final Set<HsProfileScopeRealEntity> expectedPatchedScopeEntities = Set.of(initialScopeEntity2,
|
||||
newScopeEntity3);
|
||||
|
||||
@Mock
|
||||
private EntityManager em;
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
// Mock em.find for scopes that are part of the patch and need to be fetched
|
||||
lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_1))).thenReturn(initialScopeEntity1);
|
||||
lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_2))).thenReturn(initialScopeEntity2);
|
||||
lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_3))).thenReturn(newScopeEntity3);
|
||||
|
||||
val patchScopeResource2 = new ScopeResource();
|
||||
patchScopeResource2.setUuid(SCOPE_UUID_2);
|
||||
patchScopeResource2.setType("SSH");
|
||||
patchScopeResource2.setQualifier("dev");
|
||||
|
||||
val patchScopeResource3 = new ScopeResource();
|
||||
patchScopeResource3.setUuid(SCOPE_UUID_3);
|
||||
patchScopeResource3.setType("HSADMIN");
|
||||
patchScopeResource3.setQualifier("test");
|
||||
|
||||
patchedScopeResources = List.of(patchScopeResource2, patchScopeResource3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsProfileEntity newInitialEntity() {
|
||||
final var entity = new HsProfileEntity();
|
||||
entity.setUuid(INITIAL_PROFILE_UUID);
|
||||
entity.setActive(INITIAL_ACTIVE);
|
||||
entity.setEmailAddress(INITIAL_EMAIL_ADDRESS);
|
||||
entity.setTotpSecrets(INITIAL_TOTP_SECRETS);
|
||||
entity.setSmsNumber(INITIAL_SMS_NUMBER);
|
||||
entity.setPhonePassword(INITIAL_PHONE_PASSWORD);
|
||||
// Ensure scopes is a mutable set for the patcher
|
||||
entity.setScopes(new HashSet<>(initialScopeEntities));
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProfilePatchResource newPatchResource() {
|
||||
return new ProfilePatchResource();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HsProfileEntityPatcher createPatcher(final HsProfileEntity entity) {
|
||||
final var scopeMapper = new ScopeResourceToEntityMapper(em, mock(MessageTranslator.class));
|
||||
return new HsProfileEntityPatcher(scopeMapper, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Property> propertyTestDescriptors() {
|
||||
return Stream.of(
|
||||
new SimpleProperty<>(
|
||||
"active",
|
||||
ProfilePatchResource::setActive,
|
||||
PATCHED_ACTIVE,
|
||||
HsProfileEntity::setActive,
|
||||
PATCHED_ACTIVE)
|
||||
.notNullable(),
|
||||
new JsonNullableProperty<>(
|
||||
"emailAddress",
|
||||
ProfilePatchResource::setEmailAddress,
|
||||
PATCHED_EMAIL_ADDRESS,
|
||||
HsProfileEntity::setEmailAddress,
|
||||
PATCHED_EMAIL_ADDRESS),
|
||||
new SimpleProperty<>(
|
||||
"totpSecret",
|
||||
ProfilePatchResource::setTotpSecrets,
|
||||
PATCHED_TOTP_SECRETS,
|
||||
HsProfileEntity::setTotpSecrets,
|
||||
PATCHED_TOTP_SECRETS)
|
||||
.notNullable(),
|
||||
new JsonNullableProperty<>(
|
||||
"smsNumber",
|
||||
ProfilePatchResource::setSmsNumber,
|
||||
PATCHED_SMS_NUMBER,
|
||||
HsProfileEntity::setSmsNumber,
|
||||
PATCHED_SMS_NUMBER),
|
||||
new JsonNullableProperty<>(
|
||||
"phonePassword",
|
||||
ProfilePatchResource::setPhonePassword,
|
||||
PATCHED_PHONE_PASSWORD,
|
||||
HsProfileEntity::setPhonePassword,
|
||||
PATCHED_PHONE_PASSWORD),
|
||||
new SimpleProperty<>(
|
||||
"scopes",
|
||||
ProfilePatchResource::setScopes,
|
||||
patchedScopeResources,
|
||||
HsProfileEntity::setScopes,
|
||||
expectedPatchedScopeEntities)
|
||||
.notNullable()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HsProfileEntityUnitTest {
|
||||
|
||||
static final HsProfileEntity GIVEN_PROFILE_ENTITY = HsProfileEntity.builder()
|
||||
.uuid(UUID.fromString("11111111-1111-1111-1111-111111111111"))
|
||||
.subject(
|
||||
RealSubjectEntity.builder().uuid(UUID.randomUUID()).name("testSubject").build())
|
||||
.person(
|
||||
HsOfficePersonRealEntity.builder()
|
||||
.personType(HsOfficePersonType.NATURAL_PERSON)
|
||||
.familyName("Miller")
|
||||
.givenName("John")
|
||||
.build()
|
||||
)
|
||||
.emailAddress("john.miller@example.com")
|
||||
.smsNumber("+49 1234567890")
|
||||
.globalUid(10001)
|
||||
.globalUid(20002)
|
||||
.phonePassword("hello world")
|
||||
.totpSecrets(List.of("secret1", "secret2"))
|
||||
.active(true)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void toShortStringContainsJustTypeAndQualifier() {
|
||||
assertThat(GIVEN_PROFILE_ENTITY.toShortString()).isEqualTo("true:john.miller@example.com:20002");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContainsAllPropertiesExceptUuidAndPasswordHash() {
|
||||
assertThat(GIVEN_PROFILE_ENTITY.toString()).isEqualTo("profile(true, john.miller@example.com, [secret1, secret2], hello world, +49 1234567890)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPasswordSetsPasswordHash() {
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPassword("my password");
|
||||
assertThat(profile.getPasswordHash()).startsWith("{SSHA}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsValidSshaPasswordHash() {
|
||||
val givenSshaHash = "{SSHA}SNBnIh5QomfgrvDLDwBR+JOcc8Y17H+4";
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPasswordHash(givenSshaHash);
|
||||
assertThat(profile.getPasswordHash()).isEqualTo(givenSshaHash);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsValidArgon2PasswordHash() {
|
||||
val givenArgon2Hash = "{ARGON2}$argon2id$v=19$m=65536,t=3,p=1$pEabRksh7EJQV+OwPR5n7Q$83qQtZe2J8+fteWm7g/uvXksfhJKGsipZFsuAaJtBjs";
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPasswordHash(givenArgon2Hash);
|
||||
assertThat(profile.getPasswordHash()).isEqualTo(givenArgon2Hash);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectInvalidPasswordHash() {
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
val throwable = assertThrows(
|
||||
ValidationException.class,
|
||||
() -> profile.setPasswordHash("{whatever} but not a valid hash"));
|
||||
assertThat(throwable.getMessage()).isEqualTo("passwordHash must be SSHA or ARGON2 hash valid for LDAP");
|
||||
}
|
||||
}
|
||||
-210
@@ -1,210 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.hostsharing.hsadminng.config.WebSecurityConfigForWebMvcTests;
|
||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
|
||||
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.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.SynchronizationType;
|
||||
|
||||
@WebMvcTest(HsProfileScopeController.class)
|
||||
@Import({ StrictMapper.class,
|
||||
MessageTranslator.class,
|
||||
JsonObjectMapperConfiguration.class,
|
||||
WebSecurityConfigForWebMvcTests.class })
|
||||
@ActiveProfiles({"fake-jwt", "test"})
|
||||
class HsProfileScopeControllerRestTest {
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
Context contextMock;
|
||||
|
||||
@Autowired
|
||||
@SuppressWarnings("unused") // not used in test but in controller class
|
||||
StrictMapper mapper;
|
||||
|
||||
@MockitoBean
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@MockitoBean
|
||||
EntityManagerFactory emf;
|
||||
|
||||
@MockitoBean
|
||||
HsProfileScopeRbacRepository scopeRbacRepo;
|
||||
|
||||
@TestConfiguration
|
||||
public static class TestConfig {
|
||||
@Bean
|
||||
public EntityManager entityManager() {
|
||||
return mock(EntityManager.class);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getListOfScopesReturnsOkWithEmptyList() throws Exception {
|
||||
|
||||
// given
|
||||
givenNoScopesInTheRepository();
|
||||
|
||||
// when
|
||||
mockMvc.perform(MockMvcRequestBuilders
|
||||
.get("/api/hs/accounts/scopes")
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(print())
|
||||
|
||||
// then
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getListOfScopesReturnsAllScopesForGlobalAdmin() throws Exception {
|
||||
|
||||
// given
|
||||
givenSomeScopesInTheRepository();
|
||||
when(contextMock.isGlobalAdmin()).thenReturn(true);
|
||||
|
||||
// when
|
||||
mockMvc.perform(MockMvcRequestBuilders
|
||||
.get("/api/hs/accounts/scopes")
|
||||
.header("Authorization", bearer("Bearer superuser-alex@hostsharing.net"))
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(print())
|
||||
|
||||
// then
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath(
|
||||
"$", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"type": "HSADMIN",
|
||||
"qualifier": "prod",
|
||||
"onlyForNaturalPersons": true,
|
||||
"publicAccess": true
|
||||
},
|
||||
{
|
||||
"type": "SSH",
|
||||
"qualifier": "public",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": true
|
||||
},
|
||||
{
|
||||
"type": "SSH",
|
||||
"qualifier": "internal",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": false
|
||||
}
|
||||
]
|
||||
"""
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getListOfScopesReturnsOnlyPublicScopesForNormalUser() throws Exception {
|
||||
|
||||
// given
|
||||
givenSomeScopesInTheRepository();
|
||||
when(contextMock.isGlobalAdmin()).thenReturn(false);
|
||||
|
||||
// when
|
||||
mockMvc.perform(MockMvcRequestBuilders
|
||||
.get("/api/hs/accounts/scopes")
|
||||
.header("Authorization", bearer("drew@hostsharing.org"))
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(print())
|
||||
|
||||
// then
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath(
|
||||
"$", lenientlyEquals("""
|
||||
[
|
||||
{
|
||||
"type": "HSADMIN",
|
||||
"qualifier": "prod",
|
||||
"onlyForNaturalPersons": true,
|
||||
"publicAccess": true
|
||||
},
|
||||
{
|
||||
"type": "SSH",
|
||||
"qualifier": "public",
|
||||
"onlyForNaturalPersons": false,
|
||||
"publicAccess": true
|
||||
}
|
||||
]
|
||||
"""
|
||||
)));
|
||||
}
|
||||
|
||||
private void givenNoScopesInTheRepository() {
|
||||
when(scopeRbacRepo.findAll()).thenReturn(emptyList());
|
||||
}
|
||||
|
||||
private void givenSomeScopesInTheRepository() {
|
||||
when(scopeRbacRepo.findAll()).thenReturn(List.of(
|
||||
HsProfileScopeRbacEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("HSADMIN")
|
||||
.qualifier("prod")
|
||||
.publicAccess(true)
|
||||
.onlyForNaturalPersons(true)
|
||||
.build(),
|
||||
HsProfileScopeRbacEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("public")
|
||||
.publicAccess(true)
|
||||
.onlyForNaturalPersons(false)
|
||||
.build(),
|
||||
HsProfileScopeRbacEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("internal")
|
||||
.publicAccess(false)
|
||||
.onlyForNaturalPersons(false)
|
||||
.build()
|
||||
));
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class HsProfileScopeRbacEntityUnitTest {
|
||||
|
||||
@Test
|
||||
void toShortStringContainsJustTypeAndQualifier() {
|
||||
final var entity = HsProfileScopeRbacEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("prod")
|
||||
.publicAccess(true)
|
||||
.build();
|
||||
assertEquals("SSH:prod", entity.toShortString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContainsAllNonNullFields() {
|
||||
final var entity = HsProfileScopeRbacEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("prod")
|
||||
.publicAccess(true)
|
||||
.build();
|
||||
assertEquals("scope(SSH:prod:PUBLIC)", entity.toString());
|
||||
}
|
||||
}
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.postgresql.util.PSQLException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
@Tag("generalIntegrationTest")
|
||||
@Import({ Context.class, JpaAttempt.class })
|
||||
@Transactional
|
||||
class HsProfileScopeRbacRepositoryIntegrationTest extends ContextBasedTest {
|
||||
|
||||
// existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql)
|
||||
private static final UUID TEST_SCOPE_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID TEST_SCOPE_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
|
||||
private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
||||
|
||||
@MockitoBean
|
||||
HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
private HsProfileScopeRbacRepository scopesRepository;
|
||||
|
||||
@Test
|
||||
void shouldFindAllByNormalUserUsingTestData() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var allScopes = scopesRepository.findAll();
|
||||
|
||||
// then
|
||||
assertThat(allScopes)
|
||||
.isNotNull()
|
||||
.hasSizeGreaterThanOrEqualTo(1) // Expect at least the 1 public context from assumed test data
|
||||
.extracting(HsProfileScope::getUuid)
|
||||
.contains(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAllByAdminUserUsingTestData() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var allScopes = scopesRepository.findAll();
|
||||
|
||||
// then
|
||||
assertThat(allScopes)
|
||||
.isNotNull()
|
||||
.hasSizeGreaterThanOrEqualTo(3); // Expect at least the 1 public scope from assumed test data
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByUuidUsingTestData() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopesRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
assertThat(foundEntityOptional).map(Object::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByTypeAndQualifierUsingTestData() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopesRepository.findByTypeAndQualifier("SSH", "internal");
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
assertThat(foundEntityOptional).map(Object::toString).contains("scope(SSH:internal:NP-ONLY:INTERNAL)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyOptionalWhenFindByTypeAndQualifierNotFound() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// given
|
||||
final var nonExistentQualifier = "non-existent-qualifier";
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopesRepository.findByTypeAndQualifier(
|
||||
"HSADMIN", nonExistentQualifier);
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveNewScope() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// given
|
||||
final var newQualifier = "test@example.social";
|
||||
final var newType = "MASTODON";
|
||||
final var newScope = HsProfileScopeRbacEntity.builder()
|
||||
.type(newType)
|
||||
.qualifier(newQualifier)
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var savedEntity = scopesRepository.save(newScope);
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// then
|
||||
assertThat(savedEntity).isNotNull();
|
||||
final var generatedUuid = savedEntity.getUuid();
|
||||
assertThat(generatedUuid).isNotNull(); // Verify UUID was generated
|
||||
|
||||
// Fetch again using the generated UUID to confirm persistence
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME); // Re-set context if needed after clear
|
||||
final var foundEntityOptional = scopesRepository.findByUuid(generatedUuid);
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
final var foundEntity = foundEntityOptional.get();
|
||||
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
||||
assertThat(foundEntity.getType()).isEqualTo(newType);
|
||||
assertThat(foundEntity.getQualifier()).isEqualTo(newQualifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventUpdateOfExistingScope() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// given an existing entity from test data
|
||||
final var entityToUpdateOptional = scopesRepository.findByUuid(TEST_SCOPE_MATRIX_INTERNAL_UUID);
|
||||
assertThat(entityToUpdateOptional)
|
||||
.withFailMessage("Could not find existing scope with UUID %s. Ensure test data exists.",
|
||||
TEST_SCOPE_MATRIX_INTERNAL_UUID)
|
||||
.isPresent();
|
||||
final var entityToUpdate = entityToUpdateOptional.get();
|
||||
|
||||
// when
|
||||
entityToUpdate.setQualifier("updated");
|
||||
final var exception = catchThrowable( () -> {
|
||||
scopesRepository.save(entityToUpdate);
|
||||
em.flush();
|
||||
});
|
||||
|
||||
// then
|
||||
assertThat(exception)
|
||||
.isInstanceOf(PersistenceException.class)
|
||||
.hasCauseInstanceOf(PSQLException.class)
|
||||
.hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed.");
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class HsProfileScopeRealEntityUnitTest {
|
||||
|
||||
@Test
|
||||
void toShortStringContainsJustTypeAndQualifier() {
|
||||
final var entity = HsProfileScopeRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("prod")
|
||||
.publicAccess(true)
|
||||
.build();
|
||||
assertEquals("SSH:prod", entity.toShortString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContainsAllNonNullFields() {
|
||||
final var entity = HsProfileScopeRealEntity.builder()
|
||||
.uuid(UUID.randomUUID())
|
||||
.type("SSH")
|
||||
.qualifier("prod")
|
||||
.publicAccess(true)
|
||||
.build();
|
||||
assertEquals("scope(SSH:prod:PUBLIC)", entity.toString());
|
||||
}
|
||||
}
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.postgresql.util.PSQLException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
@Tag("generalIntegrationTest")
|
||||
@Import({ Context.class, JpaAttempt.class })
|
||||
class HsProfileScopeRealRepositoryIntegrationTest extends ContextBasedTest {
|
||||
|
||||
// existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql)
|
||||
private static final UUID TEST_SCOPE_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
private static final UUID TEST_SCOPE_SSH_INTERNAL_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
private static final UUID TEST_SCOPE_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
|
||||
private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
||||
|
||||
@MockitoBean
|
||||
HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
private HsProfileScopeRealRepository scopeRepository;
|
||||
|
||||
@Test
|
||||
public void historizationIsAvailable() {
|
||||
// given
|
||||
final String nativeQuerySql = "select * from hs_accounts.scope_hv";
|
||||
|
||||
// when
|
||||
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
||||
final var query = em.createNativeQuery(nativeQuerySql);
|
||||
final var rowsBefore = query.getResultList();
|
||||
|
||||
// then
|
||||
assertThat(rowsBefore)
|
||||
.as("hs_accounts.scope_hv only contain no rows for a timestamp before test data creation")
|
||||
.hasSize(0);
|
||||
|
||||
// and when
|
||||
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
|
||||
em.createNativeQuery(nativeQuerySql, Integer.class);
|
||||
final var rowsAfter = query.getResultList();
|
||||
|
||||
// then
|
||||
assertThat(rowsAfter)
|
||||
.as("hs_accounts.scope_hv should now contain the test-data rows for the current timestamp")
|
||||
.hasSize(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindAllUsingTestData() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var allScopes = scopeRepository.findAll();
|
||||
|
||||
// then
|
||||
assertThat(allScopes)
|
||||
.isNotNull()
|
||||
.hasSizeGreaterThanOrEqualTo(3) // Expect at least the 3 from assumed test data
|
||||
.extracting(HsProfileScope::getUuid)
|
||||
.contains(TEST_SCOPE_HSADMIN_PROD_UUID, TEST_SCOPE_SSH_INTERNAL_UUID, TEST_SCOPE_MATRIX_INTERNAL_UUID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByUuidUsingTestData() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopeRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
assertThat(foundEntityOptional).map(Object::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByTypeAndQualifierUsingTestData() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopeRepository.findByTypeAndQualifier("SSH", "internal");
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
assertThat(foundEntityOptional).map(Object::toString).contains("scope(SSH:internal:NP-ONLY:INTERNAL)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyOptionalWhenFindByTypeAndQualifierNotFound() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// given
|
||||
final var nonExistentQualifier = "non-existent-qualifier";
|
||||
|
||||
// when
|
||||
final var foundEntityOptional = scopeRepository.findByTypeAndQualifier(
|
||||
"HSADMIN", nonExistentQualifier);
|
||||
|
||||
// then
|
||||
assertThat(foundEntityOptional).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveNewScope() {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
|
||||
// given
|
||||
final var newQualifier = "test@example.social";
|
||||
final var newType = "MASTODON";
|
||||
final var newScope = HsProfileScopeRealEntity.builder()
|
||||
.type(newType)
|
||||
.qualifier(newQualifier)
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var savedEntity = scopeRepository.save(newScope);
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// then
|
||||
assertThat(savedEntity).isNotNull();
|
||||
final var generatedUuid = savedEntity.getUuid();
|
||||
assertThat(generatedUuid).isNotNull(); // Verify UUID was generated
|
||||
|
||||
// Fetch again using the generated UUID to confirm persistence
|
||||
context(TEST_USER_SUBJECT_NAME); // Re-set context if needed after clear
|
||||
final var foundEntityOptional = scopeRepository.findByUuid(generatedUuid);
|
||||
assertThat(foundEntityOptional).isPresent();
|
||||
final var foundEntity = foundEntityOptional.get();
|
||||
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
||||
assertThat(foundEntity.getType()).isEqualTo(newType);
|
||||
assertThat(foundEntity.getQualifier()).isEqualTo(newQualifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventUpdateOfExistingScope() {
|
||||
context(TEST_USER_SUBJECT_NAME);
|
||||
|
||||
// given an existing entity from test data
|
||||
final var entityToUpdateOptional = scopeRepository.findByUuid(TEST_SCOPE_MATRIX_INTERNAL_UUID);
|
||||
assertThat(entityToUpdateOptional)
|
||||
.withFailMessage("Could not find existing Scope with UUID %s. Ensure test data exists.",
|
||||
TEST_SCOPE_MATRIX_INTERNAL_UUID)
|
||||
.isPresent();
|
||||
final var entityToUpdate = entityToUpdateOptional.get();
|
||||
|
||||
// when
|
||||
entityToUpdate.setQualifier("updated");
|
||||
final var exception = catchThrowable( () -> {
|
||||
scopeRepository.save(entityToUpdate);
|
||||
em.flush();
|
||||
});
|
||||
|
||||
// then
|
||||
assertThat(exception)
|
||||
.isInstanceOf(PersistenceException.class)
|
||||
.hasCauseInstanceOf(PSQLException.class)
|
||||
.hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed.");
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class AccountCanViewTheirOwnMemberships extends BaseAccountUseCase<AccountCanViewTheirOwnMemberships> {
|
||||
|
||||
public AccountCanViewTheirOwnMemberships(final ScenarioTest scenarioTest, final FakeLoginUser asLoginUser) {
|
||||
super(scenarioTest, asLoginUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
obtain(
|
||||
"personUuid",
|
||||
() -> httpGet( asLoginUser, "/api/hs/accounts/accounts")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].person.uuid"),
|
||||
"Fetch the account for the current subject to resolve the related person."
|
||||
);
|
||||
|
||||
withTitle("Resolve partner trade name", () ->
|
||||
httpGet( asLoginUser,
|
||||
"/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{personUuid}")
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1)
|
||||
.extractValue("[0].anchor.tradeName", "partnerTradeName")
|
||||
);
|
||||
|
||||
withTitle("Resolve partner UUID", () ->
|
||||
httpGet( asLoginUser,
|
||||
"/api/hs/office/partners?name=" + uriEncoded("%{partnerTradeName}")
|
||||
)
|
||||
.expecting(OK).expecting(JSON).expectArrayElements(1)
|
||||
.extractUuidAlias("[0].uuid", "partnerUuid")
|
||||
);
|
||||
|
||||
return withTitle("View their memberships", () ->
|
||||
httpGet( asLoginUser, "/api/hs/office/memberships?partnerUuid=%{partnerUuid}")
|
||||
.expecting(OK).expecting(JSON)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final HttpResponse response) {
|
||||
final var expectedMembershipsJson = ScenarioTest.resolve("%{expectedMembershipsJson}", DROP_COMMENTS);
|
||||
lenientlyEquals(expectedMembershipsJson).matches(response.getBody());
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asSubject;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class AccountCanViewTheirOwnPerson extends BaseAccountUseCase<AccountCanViewTheirOwnPerson> {
|
||||
|
||||
public AccountCanViewTheirOwnPerson(final ScenarioTest scenarioTest, final FakeLoginUser asLoginUser) {
|
||||
super(scenarioTest, asLoginUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
obtain(
|
||||
"personUuid",
|
||||
() -> httpGet( asSubject("%{subjectName}"),
|
||||
"/api/hs/accounts/accounts"
|
||||
)
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].person.uuid"),
|
||||
"Fetch the account for the current subject to resolve the related person."
|
||||
);
|
||||
|
||||
return withTitle("View Own Person", () ->
|
||||
httpGet( asSubject("%{subjectName}"),
|
||||
"/api/hs/office/persons/%{personUuid}"
|
||||
)
|
||||
.expecting(OK).expecting(JSON)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final HttpResponse response) {
|
||||
path("uuid").contains("%{personUuid}").accept(response);
|
||||
path("givenName").contains("%{personGivenName}").accept(response);
|
||||
path("familyName").contains("%{personFamilyName}").accept(response);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asSubject;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class AccountCanViewTheirOwnRelations extends BaseAccountUseCase<AccountCanViewTheirOwnRelations> {
|
||||
|
||||
public AccountCanViewTheirOwnRelations(final ScenarioTest scenarioTest, final FakeLoginUser asLoginUser) {
|
||||
super(scenarioTest, asLoginUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
obtain(
|
||||
"personUuid",
|
||||
() -> httpGet( asSubject("%{subjectName}"), "/api/hs/accounts/accounts")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].person.uuid"),
|
||||
"Fetch the account for the current subject to resolve the related person."
|
||||
);
|
||||
|
||||
return withTitle("View their relations", () ->
|
||||
httpGet(asSubject("%{subjectName}"), "/api/hs/office/relations?personUuid%{personUuid}")
|
||||
.expecting(OK).expecting(JSON)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final HttpResponse response) {
|
||||
val expectedRelationsJson = ScenarioTest.resolve("%{expectedRelationsJson}", DROP_COMMENTS);
|
||||
lenientlyEquals(expectedRelationsJson).matches(response.getBody());
|
||||
}
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.Produces;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.Requires;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asSubject;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asGlobalAgent;
|
||||
|
||||
class AccountScenarioTests extends ScenarioTest {
|
||||
|
||||
@SneakyThrows
|
||||
@BeforeEach
|
||||
protected void beforeScenario(final TestInfo testInfo) {
|
||||
super.beforeScenario(testInfo);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(90)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class RbacContextScenarios {
|
||||
|
||||
@Test
|
||||
@Order(9010)
|
||||
@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)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(91)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class CurrentLoginUserScenarios {
|
||||
|
||||
@Test
|
||||
@Order(9110)
|
||||
@Produces("Current Login User")
|
||||
void shouldFetchCurrentLoginUser() {
|
||||
new CurrentLoginUser(scenarioTest)
|
||||
.given("subjectName", "superuser-fran@hostsharing.net")
|
||||
.given("personGivenName", "Fran")
|
||||
.given("expectedToBeGlobalAdmin", true)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(92)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class ScenariosForExistingPersons {
|
||||
|
||||
@Test
|
||||
@Order(9210)
|
||||
@Produces(
|
||||
explicitly = "Account: peter-smith",
|
||||
implicitly = { "Person: Peter Smith" })
|
||||
void shouldCreateInitialAccountForExistingNaturalPerson() {
|
||||
new CreateAccountForExistingPerson(scenarioTest, asGlobalAgent())
|
||||
// to find a specific existing person
|
||||
.given("personFamilyName", "Smith")
|
||||
.given("personGivenName", "Peter")
|
||||
.given("personGivenType", "NATURAL_PERSON")
|
||||
// a login name, to be stored in the new RBAC subject
|
||||
.given("subjectName", "xyz-peter.smith")
|
||||
// initial account
|
||||
.given("globalUid", 21011)
|
||||
.given("globalGid", 21011)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9211)
|
||||
@Requires("Account: peter-smith")
|
||||
void newlyCreatedAccountForExistingNaturalPersonShouldBeAbleToViewThatPerson() {
|
||||
new AccountCanViewTheirOwnPerson(scenarioTest, asSubject("xyz-peter.smith"))
|
||||
// to find a specific existing person
|
||||
.given("subjectName", "xyz-peter.smith")
|
||||
// some expected person data
|
||||
.expected("personFamilyName", "Smith")
|
||||
.expected("personGivenName", "Peter")
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9212)
|
||||
@Requires("Account: peter-smith")
|
||||
void newlyCreatedAccountForExistingNaturalPersonShouldBeAbleToViewExistingRelations() {
|
||||
new AccountCanViewTheirOwnRelations(scenarioTest, asSubject("xyz-peter.smith"))
|
||||
// to find a specific existing person
|
||||
.given("subjectName", "xyz-peter.smith")
|
||||
// some expected person data ... which might change if test-data changes
|
||||
.expected("expectedRelationsJson", """
|
||||
[
|
||||
{
|
||||
"type": "PARTNER",
|
||||
"mark": null,
|
||||
"anchor": { "tradeName": "Hostsharing eG" },
|
||||
"holder": { "tradeName": "Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K." },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } }
|
||||
},
|
||||
{
|
||||
"type": "DEBITOR",
|
||||
"mark": null,
|
||||
"anchor": { "tradeName": "Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K." },
|
||||
"holder": { "tradeName": "Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K." },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } }
|
||||
},
|
||||
{
|
||||
"type": "REPRESENTATIVE",
|
||||
"mark": null,
|
||||
"anchor": { "tradeName": "Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K." },
|
||||
"holder": { "givenName": "Peter", "familyName": "Smith" },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } }
|
||||
},
|
||||
{
|
||||
"type": "PARTNER",
|
||||
"mark": null,
|
||||
"anchor": { "tradeName": "Hostsharing eG" },
|
||||
"holder": { "givenName": "Peter", "familyName": "Smith" },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@sixthcontact.example.com" } }
|
||||
},
|
||||
{
|
||||
"type": "DEBITOR",
|
||||
"mark": null,
|
||||
"anchor": { "givenName": "Peter", "familyName": "Smith" },
|
||||
"holder": { "givenName": "Peter", "familyName": "Smith" },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@thirdcontact.example.com" } }
|
||||
},
|
||||
{
|
||||
"type": "SUBSCRIBER",
|
||||
"mark": "members-announce",
|
||||
"anchor": { "tradeName": "Third OHG" },
|
||||
"holder": { "givenName": "Peter", "familyName": "Smith" },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@thirdcontact.example.com" } }
|
||||
}
|
||||
]
|
||||
""")
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9213)
|
||||
@Requires("Account: peter-smith")
|
||||
void newlyCreatedAccountForExistingNaturalPersonShouldBeAbleToViewExistingMemberships() {
|
||||
new AccountCanViewTheirOwnMemberships(scenarioTest, asSubject("xyz-peter.smith"))
|
||||
// to find a specific existing person
|
||||
.given("subjectName", "xyz-peter.smith")
|
||||
// some expected membership data ... which might change if test-data changes
|
||||
.expected("expectedMembershipsJson", """
|
||||
[
|
||||
{
|
||||
"partner": {
|
||||
"partnerNumber": "P-10002",
|
||||
"partnerRel": {
|
||||
"type": "PARTNER",
|
||||
"anchor": { "tradeName": "Hostsharing eG" },
|
||||
"holder": { "tradeName": "Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K." },
|
||||
"contact": { "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } }
|
||||
},
|
||||
"details": {
|
||||
"registrationOffice": "Hamburg",
|
||||
"registrationNumber": "RegNo123456789"
|
||||
}
|
||||
},
|
||||
"memberNumber": "M-1000202",
|
||||
"memberNumberSuffix": "02",
|
||||
"validFrom": "2022-10-01",
|
||||
"validTo": "2025-12-31",
|
||||
"status": "CANCELLED",
|
||||
"membershipFeeBillable": true
|
||||
}
|
||||
]
|
||||
""")
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(93)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class ScenariosForImplicitlyCreatedPersons {
|
||||
|
||||
@Test
|
||||
@Order(9310)
|
||||
@Produces(
|
||||
explicitly = "Account: peter-newman",
|
||||
implicitly = { "Person: Peter Newman" })
|
||||
void shouldCreateInitialAccountForNewNaturalPerson() {
|
||||
new CreateAccountForNewPerson(scenarioTest, asGlobalAgent())
|
||||
// to find a specific existing person
|
||||
.given("personFamilyName", "Newman")
|
||||
.given("personGivenName", "Peter")
|
||||
// a login name, to be stored in the new RBAC subject
|
||||
.given("subjectName", "xyz-peter.newman")
|
||||
// initial account
|
||||
.given("globalUid", 21012)
|
||||
.given("globalGid", 21012)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9311)
|
||||
@Requires("Account: peter-newman")
|
||||
void newlyCreatedAccountForNewNaturalPersonShouldBeAbleToViewThatPerson() {
|
||||
new AccountCanViewTheirOwnPerson(scenarioTest, asSubject("xyz-peter.newman"))
|
||||
// to find a specific existing person
|
||||
.given("subjectName", "xyz-peter.newman")
|
||||
// some expected person data
|
||||
.expected("personFamilyName", "Newman")
|
||||
.expected("personGivenName", "Peter")
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||
|
||||
public abstract class BaseAccountUseCase<T extends UseCase<?>> extends UseCase<T> {
|
||||
|
||||
protected final FakeLoginUser asLoginUser;
|
||||
|
||||
public BaseAccountUseCase(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
super(testSuite);
|
||||
this.asLoginUser = asLoginUser;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asGlobalAgent;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public abstract class BaseProfileUseCase<T extends UseCase<?>> extends UseCase<T> {
|
||||
|
||||
protected final FakeLoginUser asLoginUser;
|
||||
|
||||
public BaseProfileUseCase(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
super(testSuite);
|
||||
this.asLoginUser = asLoginUser;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
protected ScopeResource[] fetchScopeResourcesByDescriptorPairs(final String descriptPairsVarName) {
|
||||
final var requestedScopes = ScenarioTest.getTypedVariable("scopes", Pair[].class);
|
||||
final var existingScopesJson = withTitle("Fetch Available Account Scopes", () ->
|
||||
httpGet(asGlobalAgent(), "/api/hs/accounts/scopes").expecting(OK).expecting(JSON)
|
||||
).getResponse().body();
|
||||
final var existingScopes = objectMapper.readValue(existingScopesJson, ScopeResource[].class);
|
||||
return Arrays.stream(requestedScopes)
|
||||
.map(pair -> Arrays.stream(existingScopes)
|
||||
.filter(scope -> scope.getType().equals(pair.getLeft())
|
||||
&& scope.getQualifier().equals(pair.getRight()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"No matching scope found for type=" + pair.getLeft()
|
||||
+ " and qualifier=" + pair.getRight()))
|
||||
)
|
||||
.toArray(ScopeResource[]::new);
|
||||
}
|
||||
}
|
||||
+14
-26
@@ -9,42 +9,31 @@ import org.springframework.http.HttpStatus;
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreateProfileForExistingPerson extends BaseProfileUseCase<CreateProfileForExistingPerson> {
|
||||
public class CreateAccountForExistingPerson extends BaseAccountUseCase<CreateAccountForExistingPerson> {
|
||||
|
||||
public CreateProfileForExistingPerson(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
public CreateAccountForExistingPerson(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
super(testSuite, asLoginUser);
|
||||
|
||||
introduction("A set of profile contains the login data for an RBAC subject.");
|
||||
introduction("An account combines an RBAC subject with a natural person and thus grant's access to data in hsadmin-NG.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
obtain("Person: %{personGivenName} %{personFamilyName}", () ->
|
||||
httpGet(asLoginUser, "/api/hs/office/persons?name=%{personFamilyName}")
|
||||
httpGet(asLoginUser, "/api/hs/office/persons?name=%{personFamilyName}&type=%{personGivenType}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||
"In real situations we have more precise measures to find the related person."
|
||||
);
|
||||
|
||||
given("resolvedScopes",
|
||||
fetchScopeResourcesByDescriptorPairs("scopes")
|
||||
);
|
||||
|
||||
return obtain("newProfile", () ->
|
||||
httpPost(asLoginUser, "/api/hs/accounts/profiles", usingJsonBody("""
|
||||
return obtain("newAccount", () ->
|
||||
httpPost(asLoginUser, "/api/hs/accounts/accounts", usingJsonBody("""
|
||||
{
|
||||
"person.uuid": ${Person: %{personGivenName} %{personFamilyName}},
|
||||
"nickname": ${nickname},
|
||||
"emailAddress": ${emailAddress},
|
||||
"smsNumber": ${smsNumber},
|
||||
"password": ${password},
|
||||
"totpSecrets": @{totpSecrets},
|
||||
"phonePassword": ${phonePassword},
|
||||
"subjectName": ${subjectName},
|
||||
"globalUid": %{globalUid},
|
||||
"globalGid": %{globalGid},
|
||||
"active": %{active},
|
||||
"scopes": @{resolvedScopes}
|
||||
"globalGid": %{globalGid}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
@@ -52,15 +41,14 @@ public class CreateProfileForExistingPerson extends BaseProfileUseCase<CreatePro
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final UseCase<CreateProfileForExistingPerson>.HttpResponse response) {
|
||||
protected void verify(final UseCase<CreateAccountForExistingPerson>.HttpResponse response) {
|
||||
verify(
|
||||
"Verify the new Profile",
|
||||
() -> httpGet(asLoginUser, "/api/hs/accounts/profiles/%{newProfile}")
|
||||
"Verify the new Account",
|
||||
() -> httpGet(asLoginUser, "/api/hs/accounts/accounts/%{newAccount}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
path("uuid").contains("%{newProfile}"),
|
||||
path("nickname").contains("%{nickname}"),
|
||||
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"),
|
||||
path("totpSecrets").contains("@{totpSecrets}")
|
||||
path("uuid").contains("%{newAccount}"),
|
||||
path("subjectName").contains("%{subjectName}"),
|
||||
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}")
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-25
@@ -9,23 +9,19 @@ import org.springframework.http.HttpStatus;
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class CreateProfileForNewPerson extends BaseProfileUseCase<CreateProfileForNewPerson> {
|
||||
public class CreateAccountForNewPerson extends BaseAccountUseCase<CreateAccountForNewPerson> {
|
||||
|
||||
public CreateProfileForNewPerson(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
public CreateAccountForNewPerson(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
super(testSuite, asLoginUser);
|
||||
|
||||
introduction("A set of profile contains the login data for an RBAC subject.");
|
||||
introduction("An account combines an RBAC subject with a natural person and thus grant's access to data in hsadmin-NG.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run() {
|
||||
|
||||
given("resolvedScopes",
|
||||
fetchScopeResourcesByDescriptorPairs("scopes")
|
||||
);
|
||||
|
||||
return obtain("newProfile", () ->
|
||||
httpPost(asLoginUser, "/api/hs/accounts/profiles", usingJsonBody("""
|
||||
return obtain("newAccount", () ->
|
||||
httpPost(asLoginUser, "/api/hs/accounts/accounts", usingJsonBody("""
|
||||
{
|
||||
"person": {
|
||||
"personType": "NATURAL_PERSON",
|
||||
@@ -34,16 +30,9 @@ public class CreateProfileForNewPerson extends BaseProfileUseCase<CreateProfileF
|
||||
"givenName": ${personGivenName},
|
||||
"familyName": ${personFamilyName}
|
||||
},
|
||||
"nickname": ${nickname},
|
||||
"emailAddress": ${emailAddress},
|
||||
"smsNumber": ${smsNumber},
|
||||
"password": ${password},
|
||||
"totpSecrets": @{totpSecrets},
|
||||
"phonePassword": ${phonePassword},
|
||||
"subjectName": ${subjectName},
|
||||
"globalUid": %{globalUid},
|
||||
"globalGid": %{globalGid},
|
||||
"active": %{active},
|
||||
"scopes": @{resolvedScopes}
|
||||
"globalGid": %{globalGid}
|
||||
}
|
||||
"""))
|
||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||
@@ -51,7 +40,7 @@ public class CreateProfileForNewPerson extends BaseProfileUseCase<CreateProfileF
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final UseCase<CreateProfileForNewPerson>.HttpResponse response) {
|
||||
protected void verify(final UseCase<CreateAccountForNewPerson>.HttpResponse response) {
|
||||
obtain("Person: %{personGivenName} %{personFamilyName}", () ->
|
||||
httpGet(asLoginUser, "/api/hs/office/persons?name=%{personFamilyName}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
@@ -60,13 +49,12 @@ public class CreateProfileForNewPerson extends BaseProfileUseCase<CreateProfileF
|
||||
);
|
||||
|
||||
verify(
|
||||
"Verify the new Profile",
|
||||
() -> httpGet(asLoginUser, "/api/hs/accounts/profiles/%{newProfile}")
|
||||
"Verify the new Account",
|
||||
() -> httpGet(asLoginUser, "/api/hs/accounts/accounts/%{newAccount}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
path("uuid").contains("%{newProfile}"),
|
||||
path("nickname").contains("%{nickname}"),
|
||||
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"),
|
||||
path("totpSecrets").contains("@{totpSecrets}")
|
||||
path("uuid").contains("%{newAccount}"),
|
||||
path("subjectName").contains("%{subjectName}"),
|
||||
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asGlobalAgent;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.bearerTemplate;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asSubject;
|
||||
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;
|
||||
@@ -33,10 +33,7 @@ public class CurrentLoginUser extends UseCase<CurrentLoginUser> {
|
||||
|
||||
return obtain(
|
||||
"Current Login User", () ->
|
||||
httpGet(
|
||||
"/api/hs/accounts/current", req -> req
|
||||
.header("Authorization", bearerTemplate("%{subjectName}"))
|
||||
)
|
||||
httpGet( asSubject("%{subjectName}"), "/api/hs/accounts/current")
|
||||
.expecting(OK).expecting(JSON).expectObject()
|
||||
.extractValue("subject.name", "returnedSubjectName")
|
||||
.extractValue("person.givenName", "returnedGivenName")
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.bearerTemplate;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asSubject;
|
||||
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;
|
||||
@@ -26,9 +26,8 @@ public class FetchRbacContext extends UseCase<FetchRbacContext> {
|
||||
protected HttpResponse run() {
|
||||
return obtain(
|
||||
"RBAC Context", () ->
|
||||
httpGet(
|
||||
httpGet( asSubject("%{subjectName}"),
|
||||
"/api/rbac/context", req -> req
|
||||
.header("Authorization", bearerTemplate("%{subjectName}"))
|
||||
.header("assumed-roles", resolve("%{assumedRoles}", DROP_COMMENTS))
|
||||
)
|
||||
.expecting(OK).expecting(JSON).expectObject()
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.Produces;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.Requires;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.as;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser.asGlobalAgent;
|
||||
|
||||
class ProfileScenarioTests extends ScenarioTest {
|
||||
|
||||
@SneakyThrows
|
||||
@BeforeEach
|
||||
protected void beforeScenario(final TestInfo testInfo) {
|
||||
super.beforeScenario(testInfo);
|
||||
}
|
||||
|
||||
@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)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.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)
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(30)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class ProfileScenarios {
|
||||
|
||||
@Test
|
||||
@Order(1010)
|
||||
@Produces(
|
||||
explicitly = "Profile: susan-firby",
|
||||
implicitly = {"Person: Susan Firby"})
|
||||
void shouldCreateInitialProfileForExistingNaturalPerson() {
|
||||
new CreateProfileForExistingPerson(scenarioTest, asGlobalAgent())
|
||||
// to find a specific existing person
|
||||
.given("personFamilyName", "Firby")
|
||||
.given("personGivenName", "Susan")
|
||||
// a login name, to be stored in the new RBAC subject
|
||||
.given("nickname", "firby-susan")
|
||||
// initial profile
|
||||
.given("emailAddress", "susan.firby@example.com")
|
||||
.given("smsNumber", "+49123456789")
|
||||
.given("password", "my raw password")
|
||||
.given("totpSecrets", Array.of("initialSecret"))
|
||||
.given("phonePassword", "securePass123")
|
||||
.given("globalUid", 21011)
|
||||
.given("globalGid", 21011)
|
||||
.given("active", true)
|
||||
.given(
|
||||
"scopes", Array.of(
|
||||
Pair.of("HSADMIN", "prod")
|
||||
))
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1020)
|
||||
@Requires("Profile: susan-firby")
|
||||
void naturalPersonShouldBeAbleToUpdateTheirOwnProfile() {
|
||||
new UpdateProfile(scenarioTest, as("firby-susan"))
|
||||
// the profile to update
|
||||
.given("profileUuid", "%{Profile: susan-firby}")
|
||||
// updated profile
|
||||
.given("active", false)
|
||||
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
||||
.given("emailAddress", "susan.firby@example.org")
|
||||
.given("password", "my new raw password")
|
||||
.given("phonePassword", "securePass987")
|
||||
.given("smsNumber", "+49987654321")
|
||||
.given("scopes", Array.of(Pair.of("HSADMIN", "prod"), Pair.of("SSH", "external")))
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1100)
|
||||
@Produces(
|
||||
explicitly = "Profile: peter-newman",
|
||||
implicitly = {"Person: Peter Newman"})
|
||||
void shouldCreateInitialProfileForNewNaturalPerson() {
|
||||
new CreateProfileForNewPerson(scenarioTest, asGlobalAgent())
|
||||
// to find a specific existing person
|
||||
.given("personFamilyName", "Newman")
|
||||
.given("personGivenName", "Peter")
|
||||
// a login name, to be stored in the new RBAC subject
|
||||
.given("nickname", "newman-peter")
|
||||
// initial profile
|
||||
.given("emailAddress", "peter.newman@example.com")
|
||||
.given("smsNumber", "+49123456789")
|
||||
.given("password", "my raw password")
|
||||
.given("totpSecrets", Array.of("initialSecret"))
|
||||
.given("phonePassword", "securePass123")
|
||||
.given("globalUid", 21012)
|
||||
.given("globalGid", 21012)
|
||||
.given("active", true)
|
||||
.given("scopes", Array.of(Pair.of("HSADMIN", "prod")))
|
||||
.thenExpect(HttpStatus.OK)
|
||||
.keep();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1110)
|
||||
@Requires("Profile: peter-newman")
|
||||
void newNaturalPersonShouldBeAbleToUpdateTheirOwnProfile() {
|
||||
new UpdateProfile(scenarioTest, as("newman-peter"))
|
||||
// the profile to update
|
||||
.given("profileUuid", "%{Profile: peter-newman}")
|
||||
// updated profile
|
||||
.given("active", false)
|
||||
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
||||
.given("emailAddress", "peter.newman@example.org")
|
||||
.given("password", "my new raw password")
|
||||
.given("phonePassword", "securePass987")
|
||||
.given("smsNumber", "+49987654321")
|
||||
.given(
|
||||
"scopes", Array.of(
|
||||
Pair.of("HSADMIN", "prod"),
|
||||
Pair.of("SSH", "external")
|
||||
))
|
||||
.thenExpect(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1120)
|
||||
@Requires({"Profile: peter-newman", "Profile: susan-firby"})
|
||||
// Usually, scenario tests just test positive cases, but in the case of account profiles, security is extra important,
|
||||
// thus I've added also some negative cases like this one for documentation reasons.
|
||||
// More negative cases are tested in "so-called" Acceptance and in RestTests.
|
||||
void anotherNaturalPersonShouldNotBeAbleToUpdateOthersProfile() {
|
||||
new UpdateProfile(scenarioTest, as("firby-susan"))
|
||||
// the profile to update
|
||||
.given("profileUuid", "%{Profile: peter-newman}")
|
||||
// updated profile
|
||||
.given("active", false)
|
||||
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
||||
.given("emailAddress", "peter.newman@example.org")
|
||||
.given("password", "my new raw password")
|
||||
.given("phonePassword", "securePass987")
|
||||
.given("smsNumber", "+49987654321")
|
||||
.given("scopes", Array.of(Pair.of("HSADMIN", "prod"), Pair.of("SSH", "external")))
|
||||
.thenExpect(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||
|
||||
import io.restassured.http.ContentType;
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.FakeLoginUser;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import static io.restassured.http.ContentType.JSON;
|
||||
import static org.assertj.core.api.Fail.fail;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
public class UpdateProfile extends BaseProfileUseCase<UpdateProfile> {
|
||||
|
||||
public UpdateProfile(final ScenarioTest testSuite, final FakeLoginUser asLoginUser) {
|
||||
super(testSuite, asLoginUser);
|
||||
|
||||
introduction("A set of profile contains the login data for an RBAC subject.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse run(final HttpStatus expectedStatus) {
|
||||
|
||||
given("resolvedScopes",
|
||||
fetchScopeResourcesByDescriptorPairs("scopes")
|
||||
);
|
||||
|
||||
return withTitle("Patch the Changes to the existing Profile", () -> {
|
||||
val response = httpPatch(
|
||||
asLoginUser, "/api/hs/accounts/profiles/%{profileUuid}", usingJsonBody("""
|
||||
{
|
||||
"active": %{active},
|
||||
"totpSecrets": @{totpSecrets},
|
||||
"emailAddress": ${emailAddress},
|
||||
"phonePassword": ${phonePassword},
|
||||
"smsNumber": ${smsNumber},
|
||||
"scopes": @{resolvedScopes}
|
||||
}
|
||||
"""))
|
||||
.reportWithResponse().expecting(expectedStatus);
|
||||
|
||||
return switch (expectedStatus) {
|
||||
case OK -> response.expecting(ContentType.JSON)
|
||||
.extractValue("nickname", "nickname")
|
||||
.extractValue("totpSecrets", "totpSecrets");
|
||||
case FORBIDDEN -> response.expecting(ContentType.JSON);
|
||||
default -> fail("unexpected response: " + response);
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verify(final UseCase<UpdateProfile>.HttpResponse response) {
|
||||
verify(
|
||||
"Verify the Patched Profile",
|
||||
() -> httpGet(asLoginUser, "/api/hs/accounts/profiles/%{profileUuid}")
|
||||
.expecting(OK).expecting(JSON),
|
||||
path("uuid").contains("%{newProfile}"),
|
||||
path("nickname").contains("%{nickname}"),
|
||||
path("totpSecrets").contains("%{totpSecrets}")
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -360,8 +360,9 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
|
||||
@Test
|
||||
void rejectSetupOfUnregisteredSubdomainOfUnregisteredSuperDomain() {
|
||||
domainSetupFor("sub.sub.example.org").notRegistered()
|
||||
.isRejectedWithCauseDomainNameNotFound("sub.example.org");
|
||||
Dns.fakeResultForDomain("unreg.example.org", DOMAIN_NOT_REGISTERED);
|
||||
domainSetupFor("sub.unreg.example.org").notRegistered()
|
||||
.isRejectedWithCauseDomainNameNotFound("unreg.example.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+15
@@ -72,6 +72,21 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
.body("", hasSize(17));
|
||||
// @formatter:on
|
||||
}
|
||||
@Test
|
||||
void globalAdmin_withoutAssumedRoles_canViewAllPersons_byNameAndPersonType() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||
.port(port)
|
||||
.when()
|
||||
.get("http://localhost/api/hs/office/persons")
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.body("", hasSize(17));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package net.hostsharing.hsadminng.hs.scenarios;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.config.JwtFakeBearer;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class FakeLoginUser {
|
||||
|
||||
final static String GLOBAL_AGENT = "superuser-alex@hostsharing.net"; // TODO.test: use global:AGENT when implemented
|
||||
|
||||
private String name;
|
||||
|
||||
public static FakeLoginUser as(final String name) {
|
||||
return new FakeLoginUser(name);
|
||||
public static FakeLoginUser asSubject(final String name) {
|
||||
val resolvedName = ScenarioTest.resolve( name, DROP_COMMENTS);
|
||||
return new FakeLoginUser(resolvedName);
|
||||
}
|
||||
|
||||
public static FakeLoginUser asGlobalAgent() {
|
||||
@@ -21,4 +24,8 @@ public class FakeLoginUser {
|
||||
public String bearer() {
|
||||
return JwtFakeBearer.bearer(name);
|
||||
}
|
||||
|
||||
String name() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.hostsharing.hsadminng.hs.scenarios;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.val;
|
||||
|
||||
@UtilityClass
|
||||
public class MarkdownTableCellRenderer {
|
||||
|
||||
public static String toMarkdownTableCell(final Object value) {
|
||||
val raw = String.valueOf(value);
|
||||
if (raw.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
val normalized = raw.replace("\r\n", "\n").replace("\r", "\n");
|
||||
val lines = normalized.split("\n", -1);
|
||||
val out = new StringBuilder(normalized.length() + (lines.length * 4));
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
if (i > 0) {
|
||||
out.append("<br>");
|
||||
}
|
||||
out.append(escapeWithIndent(lines[i]));
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private String escapeWithIndent(final String line) {
|
||||
int i = 0;
|
||||
while (i < line.length()) {
|
||||
final char c = line.charAt(i);
|
||||
if (c == ' ' || c == '\t') {
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
val out = new StringBuilder(line.length() + 16);
|
||||
for (int j = 0; j < i; j++) {
|
||||
out.append(line.charAt(j) == '\t' ? " " : " ");
|
||||
}
|
||||
|
||||
for (int j = i; j < line.length(); j++) {
|
||||
final char c = line.charAt(j);
|
||||
if (c == '&') {
|
||||
out.append("&");
|
||||
} else if (c == '<') {
|
||||
out.append("<");
|
||||
} else if (c == '>') {
|
||||
out.append(">");
|
||||
} else if (c == '|') {
|
||||
out.append("|");
|
||||
} else {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -274,17 +274,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
||||
//noinspection unchecked
|
||||
return (T) new BigDecimal(resolvedValue);
|
||||
}
|
||||
if (valueType == Integer.class) {
|
||||
//noinspection unchecked
|
||||
return (T) Integer.valueOf(resolvedValue);
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (T) resolvedValue;
|
||||
}
|
||||
|
||||
public static <T> T getTypedVariable(final String varName, final Class<T> expectedValueClass) {
|
||||
final var value = knowVariables().get(varName);
|
||||
if (value != null && !expectedValueClass.isAssignableFrom(value.getClass())) {
|
||||
throw new IllegalArgumentException("variable '" + varName + "'" +
|
||||
" expected to be of type " + expectedValueClass + " " +
|
||||
" but got " + value.getClass());
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class TestReport {
|
||||
|
||||
public static final File BUILD_DOC_SCENARIOS = new File("build/doc/scenarios");
|
||||
public static final SimpleDateFormat MM_DD_YYYY_HH_MM_SS = new SimpleDateFormat("MM-dd-yyyy hh:mm:ss");
|
||||
public static final SimpleDateFormat YYYY_MM_DD_HH_MM_SS = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
|
||||
|
||||
private static final File markdownLogFile = new File(BUILD_DOC_SCENARIOS, ".last-debug-log.md");
|
||||
private static final ObjectMapper objectMapper = JsonObjectMapperConfiguration.build();
|
||||
@@ -96,7 +96,7 @@ public class TestReport {
|
||||
public void close() {
|
||||
if (markdownReport != null) {
|
||||
printPara("---");
|
||||
printPara("generated on " + MM_DD_YYYY_HH_MM_SS.format(new Date()) + " for branch " + currentGitBranch());
|
||||
printPara("generated on " + YYYY_MM_DD_HH_MM_SS.format(new Date()) + " for branch " + currentGitBranch());
|
||||
markdownReport.close();
|
||||
System.out.println("SCENARIO REPORT: " + asClickableLink(markdownReportFile));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.function.Supplier;
|
||||
|
||||
import static java.net.URLEncoder.encode;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.MarkdownTableCellRenderer.toMarkdownTableCell;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.KEEP_COMMENTS;
|
||||
import static net.hostsharing.hsadminng.test.DebuggerDetection.isDebuggerAttached;
|
||||
@@ -46,7 +47,7 @@ import static org.junit.platform.commons.util.StringUtils.isNotBlank;
|
||||
public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
private static final HttpClient client = HttpClient.newHttpClient();
|
||||
private static final int HTTP_TIMEOUT_SECONDS = 20; // FIXME: configurable in environment
|
||||
private static final int HTTP_TIMEOUT_SECONDS = 20; // TODO.test: configurable in environment
|
||||
protected final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
protected final ScenarioTest testSuite;
|
||||
@@ -54,6 +55,7 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
private final Map<String, Function<String, UseCase<?>>> requirements = new LinkedMap<>();
|
||||
private final String resultAlias;
|
||||
private final Map<String, Object> givenProperties = new LinkedHashMap<>();
|
||||
private final Map<String, Object> expectedProperties = new LinkedHashMap<>();
|
||||
|
||||
private String nextTitle; // just temporary to override resultAlias for sub-use-cases
|
||||
private String introduction;
|
||||
@@ -79,13 +81,10 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
if (introduction != null) {
|
||||
testReport.printPara(introduction);
|
||||
}
|
||||
testReport.printPara("### Given Properties");
|
||||
testReport.printLine("""
|
||||
| name | value |
|
||||
|------|-------|""");
|
||||
givenProperties.forEach((key, value) ->
|
||||
testReport.printLine("| " + key + " | " + value.toString().replace("\n", "<br>") + " |"));
|
||||
testReport.printLine("");
|
||||
testReport.printPara("### Properties");
|
||||
renderProperties("Given", givenProperties);
|
||||
renderProperties("Expected", expectedProperties);
|
||||
|
||||
testReport.silent(() ->
|
||||
requirements.forEach((alias, factory) -> {
|
||||
final var resolvedAlias = ScenarioTest.resolve(alias, DROP_COMMENTS);
|
||||
@@ -106,6 +105,21 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
return response;
|
||||
}
|
||||
|
||||
private void renderProperties(final String title, final Map<String, Object> properties) {
|
||||
if (properties.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
testReport.printLine("");
|
||||
testReport.printLine("#### " + title);
|
||||
testReport.printLine("");
|
||||
testReport.printLine("""
|
||||
| name | value |
|
||||
|------|-------|""");
|
||||
properties.forEach((key, value) ->
|
||||
testReport.printLine("| " + key + " | " + toMarkdownTableCell(value) + " |"));
|
||||
testReport.printLine("");
|
||||
}
|
||||
|
||||
// this method is called by the test framework, override, but do not call from subclass
|
||||
protected HttpResponse run(final HttpStatus expectedStatus) {
|
||||
assertThat(expectedStatus).as("legacy signature only defined for HttpStatus.OK").isEqualTo(HttpStatus.OK);
|
||||
@@ -129,6 +143,15 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
// To keep things simple, both given and expected properties are available everywhere in all templates.
|
||||
// The distinction is mostly for readability.
|
||||
// It would be a bit tricky to make the expected values available just for validations.
|
||||
public final UseCase<T> expected(final String propName, final Object propValue) {
|
||||
expectedProperties.put(propName, ScenarioTest.resolve(propValue == null ? null : propValue.toString(), TemplateResolver.Resolver.KEEP_COMMENTS));
|
||||
ScenarioTest.putProperty(propName, propValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public final JsonTemplate usingJsonBody(final String jsonTemplate) {
|
||||
return new JsonTemplate(jsonTemplate);
|
||||
}
|
||||
@@ -166,23 +189,25 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpGet(
|
||||
final FakeLoginUser loginUser,
|
||||
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))
|
||||
.timeout(seconds(HTTP_TIMEOUT_SECONDS));
|
||||
.header("Authorization", loginUser.bearer())
|
||||
.header("X-Fake-Authorization", "Bearer [" + loginUser.name() + "]");
|
||||
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);
|
||||
return new HttpResponse(HttpMethod.GET, uriPath, null, response, null);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public final HttpResponse httpGet(final FakeLoginUser loginUser, final String uriPathWithPlaceholders) {
|
||||
return httpGet(uriPathWithPlaceholders,
|
||||
req -> req.header("Authorization", loginUser.bearer()));
|
||||
public final HttpResponse httpGet(
|
||||
final FakeLoginUser loginUser,
|
||||
final String uriPathWithPlaceholder) {
|
||||
return httpGet(loginUser, uriPathWithPlaceholder, requestBuilder -> requestBuilder);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@@ -196,10 +221,11 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", loginUser.bearer())
|
||||
.header("X-Fake-Authorization", "Bearer [" + loginUser.name() + "]")
|
||||
.timeout(seconds(HTTP_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response);
|
||||
return new HttpResponse(HttpMethod.POST, uriPath, requestBody, response, loginUser);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@@ -214,10 +240,11 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", loginUser.bearer())
|
||||
.header("X-Fake-Authorization", "Bearer [" + loginUser.name() + "]")
|
||||
.timeout(seconds(HTTP_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response);
|
||||
return new HttpResponse(HttpMethod.PATCH, uriPath, requestBody, response, loginUser);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@@ -228,10 +255,11 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", loginUser.bearer())
|
||||
.header("X-Fake-Authorization", "Bearer [" + loginUser.name() + "]")
|
||||
.timeout(seconds(HTTP_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
final var response = client.send(request, BodyHandlers.ofString());
|
||||
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response);
|
||||
return new HttpResponse(HttpMethod.DELETE, uriPath, null, response, loginUser);
|
||||
}
|
||||
|
||||
protected PathAssertion path(final String path) {
|
||||
@@ -289,9 +317,11 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
public final class HttpResponse {
|
||||
|
||||
private static final String AUTH_HEADER_KEY = "Authorization";
|
||||
private final HttpMethod httpMethod;
|
||||
private final String uri;
|
||||
private final String requestBody;
|
||||
private final @Nullable String authUserName;
|
||||
|
||||
@Getter
|
||||
private final java.net.http.HttpResponse<String> response;
|
||||
@@ -310,12 +340,14 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
final HttpMethod httpMethod,
|
||||
final String uri,
|
||||
final String requestBody,
|
||||
final java.net.http.HttpResponse<String> response
|
||||
final java.net.http.HttpResponse<String> response,
|
||||
final @Nullable FakeLoginUser loginUser
|
||||
) {
|
||||
this.httpMethod = httpMethod;
|
||||
this.uri = uri;
|
||||
this.requestBody = requestBody;
|
||||
this.response = response;
|
||||
this.authUserName = loginUser == null ? null : loginUser.name();
|
||||
this.status = HttpStatus.valueOf(response.statusCode());
|
||||
if (this.status == HttpStatus.CREATED) {
|
||||
final var location = response.headers().firstValue("Location").orElseThrow();
|
||||
@@ -386,6 +418,10 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <V> V getFromBody(final String path) {
|
||||
final var body = response.body();
|
||||
@@ -431,7 +467,8 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
|
||||
// the request
|
||||
testReport.printLine("```");
|
||||
testReport.printLine(httpMethod.name() + " " + uri);
|
||||
testReport.printLine(httpMethod.name() + " '" + uri + "'");
|
||||
printRequestHeaders();
|
||||
testReport.printJson(requestBody);
|
||||
|
||||
// the response
|
||||
@@ -446,6 +483,33 @@ public abstract class UseCase<T extends UseCase<?>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
private void printRequestHeaders() {
|
||||
final var request = response.request();
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
request.headers().map().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER))
|
||||
// the Authorization header with the long Bearer token is of no value here
|
||||
.filter(entry -> !AUTH_HEADER_KEY.equalsIgnoreCase(entry.getKey()))
|
||||
// instead, use the X-Fake-Authorization header as if it was the real Authorization header
|
||||
.map(entry -> Map.entry(AUTH_HEADER_KEY, entry.getValue()))
|
||||
.forEach(entry -> {
|
||||
testReport.printLine("- " + entry.getKey() + ": " + headerValue(entry));
|
||||
});
|
||||
}
|
||||
|
||||
private String headerValue(final Map.Entry<String, List<String>> entry) {
|
||||
if ("X-Fake-Authorization".equalsIgnoreCase(entry.getKey())) {
|
||||
return fakeAuthorizationValue();
|
||||
}
|
||||
return String.join(", ", entry.getValue());
|
||||
}
|
||||
|
||||
private String fakeAuthorizationValue() {
|
||||
return authUserName == null ? "" : "Bearer <" + authUserName + ">";
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void optionallyReportRequestAndResponse() {
|
||||
if (!reportGenerated) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.hostsharing.hsadminng.repr;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
class StringifyableUnitTest {
|
||||
|
||||
@Test
|
||||
void toShortString_whenEntityImplementsStringifyable_usesItsToShortString() {
|
||||
// given
|
||||
val entity = new StringifyableTestEntity(
|
||||
UUID.fromString("00000000-0000-0000-0000-000000000001"),
|
||||
"short-repr"
|
||||
);
|
||||
|
||||
// when
|
||||
val result = Stringifyable.toShortString(entity);
|
||||
|
||||
// then
|
||||
assertEquals("short-repr", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toShortString_whenEntityDoesNotImplementStringifyable_returnsUuidString() {
|
||||
// given
|
||||
val entity = new NonStringifyableTestEntity(UUID.fromString("00000000-0000-0000-0000-000000000002"));
|
||||
|
||||
// when
|
||||
val result = Stringifyable.toShortString(entity);
|
||||
|
||||
// then
|
||||
assertEquals("00000000-0000-0000-0000-000000000002", result);
|
||||
}
|
||||
|
||||
private static final class NonStringifyableTestEntity implements BaseEntity<NonStringifyableTestEntity> {
|
||||
private final UUID uuid;
|
||||
|
||||
private NonStringifyableTestEntity(final UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StringifyableTestEntity implements BaseEntity<StringifyableTestEntity>, Stringifyable {
|
||||
private final UUID uuid;
|
||||
private final String shortString;
|
||||
|
||||
private StringifyableTestEntity(final UUID uuid, final String shortString) {
|
||||
this.uuid = uuid;
|
||||
this.shortString = shortString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return shortString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,13 @@ public class JsonMatcher extends BaseMatcher<CharSequence> {
|
||||
|
||||
@Override
|
||||
public boolean matches(final Object actual) {
|
||||
if (actual == null || actual.getClass().isAssignableFrom(CharSequence.class)) {
|
||||
if (actual == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final var actualJson = new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual);
|
||||
var actualJson = (actual instanceof CharSequence)
|
||||
? actual.toString()
|
||||
: new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual);
|
||||
JSONAssert.assertEquals(expectedJson, actualJson, compareMode);
|
||||
return true;
|
||||
} catch (final JSONException | JsonProcessingException e) {
|
||||
|
||||
Reference in New Issue
Block a user