1
0

finalize PrivateCloud, Cloud- and ManagedServer and ManagedWebspace Billingtems and HostingAssets (#63)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/63
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-06-20 11:03:59 +02:00
parent 04d9b43301
commit d157730de7
20 changed files with 458 additions and 130 deletions

View File

@@ -55,9 +55,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}",
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}",
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}");
@@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
);
}
}

View File

@@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
);
}

View File

@@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// given
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 5000)
entry("Traffic", 5000),
entry("SLA-Platform EXT4H", 2),
entry("SLA-EMail", 2)
))
.subBookingItems(of(
HsBookingItemEntity.builder()
.type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
))
.build(),
HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT4H"),
entry("SLA-EMail", true)
))
.build()
))
@@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
.project(project)
.type(PRIVATE_CLOUD)
.caption("myPC")
.resources(ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 5000)
entry("Traffic", 5000),
entry("SLA-Platform EXT2H", 1),
entry("SLA-EMail", 1)
))
.subBookingItems(of(
HsBookingItemEntity.builder()
.type(MANAGED_SERVER)
.caption("myMS-1")
.resources(ofEntries(
entry("CPUs", 3),
entry("RAM", 20),
entry("SSD", 100),
entry("Traffic", 3000)
entry("Traffic", 3000),
entry("SLA-Platform", "EXT2H"),
entry("SLA-EMail", true)
))
.build(),
HsBookingItemEntity.builder()
.type(CLOUD_SERVER)
.caption("myMS-2")
.resources(ofEntries(
entry("CPUs", 2),
entry("RAM", 10),
entry("SSD", 50),
entry("Traffic", 2500)
entry("Traffic", 2500),
entry("SLA-Platform", "EXT2H"),
entry("SLA-EMail", true),
entry("SLA-Maria", true),
entry("SLA-PgSQL", true),
entry("SLA-Office", true),
entry("SLA-Web", true)
))
.build()
))
@@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// then
assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
);
"'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
"'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB",
"'D-12345:Test-Project:myPC.resources.SLA-Platform EXT2H maximum total is 1, but actual total for SLA-Platform=EXT2H is 2",
"'D-12345:Test-Project:myPC.resources.SLA-EMail' maximum total is 1, but actual total SLA-EMail is 2",
"'D-12345:Test-Project:myPC.resources.SLA-Maria' maximum total is 0, but actual total SLA-Maria is 1",
"'D-12345:Test-Project:myPC.resources.SLA-PgSQL' maximum total is 0, but actual total SLA-PgSQL is 1",
"'D-12345:Test-Project:myPC.resources.SLA-Office' maximum total is 0, but actual total SLA-Office is 1",
"'D-12345:Test-Project:myPC.resources.SLA-Web' maximum total is 0, but actual total SLA-Web is 1"
);
}
}

View File

@@ -10,8 +10,11 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@@ -28,11 +31,12 @@ import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.matchesRegex;
@Transactional
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Transactional
@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {
@LocalServerPort
@@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
JpaAttempt jpaAttempt;
@Nested
@Order(2)
class ListAssets {
@Test
@@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(3)
class AddAsset {
@Test
@@ -231,17 +237,17 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.when()
.post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"config": {}
}
"""))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.statusCode(201)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "MANAGED_WEBSPACE",
"identifier": "fir90",
"caption": "some new ManagedWebspace in client's ManagedServer",
"config": {}
}
"""))
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
.extract().header("Location"); // @formatter:on
// finally, the new asset can be accessed under the generated UUID
@@ -258,34 +264,33 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"bookingItemUuid": "%s",
"type": "MANAGED_SERVER",
"identifier": "vm1400",
"caption": "some new ManagedServer",
"config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 }
}
""".formatted(givenBookingItem.getUuid()))
.port(port)
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"bookingItemUuid": "%s",
"type": "MANAGED_SERVER",
"identifier": "vm1400",
"caption": "some new ManagedServer",
"config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 }
}
""".formatted(givenBookingItem.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/hosting/assets")
.post("http://localhost/api/hs/hosting/assets")
.then().log().all().assertThat()
.statusCode(400)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0,
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
.statusCode(400)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
}
@@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals("""
{
"statusPhrase": "Bad Request",
"message": "[
<<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found
<<<]"
"message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
}
}
@Nested
@Order(1)
class GetAsset {
@Test
@@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(4)
class PatchAsset {
@Test
@@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Nested
@Order(5)
class DeleteAsset {
@Test

View File

@@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest {
[
{
"type": "integer",
"propertyName": "monit_min_free_ssd",
"min": 1,
"max": 1000,
"propertyName": "monit_max_cpu_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_min_free_hdd",
"min": 1,
"max": 4000,
"propertyName": "monit_max_ram_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
},
{
@@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"required": false,
"defaultValue": 98,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_min_free_ssd",
"min": 1,
"max": 1000,
"required": false,
"defaultValue": 5,
"isTotalsValidator": false
},
{
@@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"min": 10,
"max": 100,
"required": false,
"defaultValue": 95,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_max_cpu_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"propertyName": "monit_min_free_hdd",
"min": 1,
"max": 4000,
"required": false,
"defaultValue": 10,
"isTotalsValidator": false
},
{
"type": "integer",
"propertyName": "monit_max_ram_usage",
"unit": "%",
"min": 10,
"max": 100,
"required": true,
"type": "boolean",
"propertyName": "software-pgsql",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-mariadb",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "enumeration",
"propertyName": "php-default",
"values": [
"5.6",
"7.0",
"7.1",
"7.2",
"7.3",
"7.4",
"8.0",
"8.1",
"8.2"
],
"required": false,
"defaultValue": "8.2",
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-5.6",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.2",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.3",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-7.4",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-php-8.2",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-postfix-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-dovecot-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-clamav",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-collabora",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-libreoffice",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
},
{
"type": "boolean",
"propertyName": "software-imagemagick-ghostscript",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
}
]
"""));
// @formatter:on
}
}

View File

@@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import jakarta.validation.ValidationException;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest {
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
// then
assertThat(result).isInstanceOf(ValidationException.class)
.hasMessageContaining(
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing");
assertThat(result).isNull(); // all required properties have defaults
}
}

View File

@@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest {
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
}
}

View File

@@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
assertThat(debitorRepo.count()).isEqualTo(count + 1);
}
@Transactional
@ParameterizedTest
@ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"})
@Transactional
public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) {
// given
context("superuser-alex@hostsharing.net");

View File

@@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import jakarta.persistence.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import static java.lang.System.out;
import static java.util.Comparator.comparing;
@@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat;
public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
private static final boolean DETAILED_BUT_SLOW_CHECK = true;
@PersistenceContext
protected EntityManager em;
@Autowired
private PlatformTransactionManager tm;
@Autowired
RbacGrantRepository rbacGrantRepo;
@@ -166,12 +173,16 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
@AfterEach
void cleanupAndCheckCleanup(final TestInfo testInfo) {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
cleanupTemporaryTestData();
deleteLeakedRbacObjects();
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
// If the whole test method has its own transaction, cleanup makes no sense.
// If that transaction even failed, cleaunup would cause an exception.
if (!tm.getTransaction(null).isRollbackOnly()) {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
cleanupTemporaryTestData();
repeatUntilTrue(3, this::deleteLeakedRbacObjects);
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
}
}
private void cleanupTemporaryTestData() {
@@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
}).assertSuccessful().returnedValue();
}
private void deleteLeakedRbacObjects() {
private boolean deleteLeakedRbacObjects() {
final var deletionSuccessful = new AtomicBoolean(true);
rbacObjectRepo.findAll().stream()
.filter(o -> o.serialId > latestIntialTestDataSerialId)
.sorted(comparing(o -> o.serialId))
@@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
if (exception != null) {
out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception);
deletionSuccessful.set(false);
}
});
return deletionSuccessful.get();
}
private void assertEqual(final Set<String> before, final Set<String> after) {
@@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
"doc/temp/" + name + ".md"
);
}
public static boolean repeatUntilTrue(int maxAttempts, Supplier<Boolean> method) {
for (int attempts = 0; attempts < maxAttempts; attempts++) {
if (method.get()) {
return true;
}
}
return false;
}
}
interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> {