diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 384fc2e3..ae13d0c8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.ValidationException; import java.util.List; import java.util.UUID; @@ -130,8 +131,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi { return entityToSave; } - @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.putConfig(KeyValueMap.from(resource.getConfig())); + if (resource.getParentAssetUuid() != null) { + entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid()) + .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted( + resource.getParentAssetUuid())))); + } }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index 52466e82..af91e0db 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -40,7 +40,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCas import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; @@ -137,7 +136,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { .importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(), dependsOnColumn("bookingItemUuid"), directlyFetchedByDependsOnColumn(), - NOT_NULL) + NULLABLE) .switchOnColumn("type", inCaseOf(CLOUD_SERVER.name(), diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 59696a23..7390c3c8 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -53,7 +53,11 @@ components: bookingItemUuid: type: string format: uuid - nullable: false + nullable: true + parentAssetUuid: + type: string + format: uuid + nullable: true type: $ref: '#/components/schemas/HsHostingAssetType' identifier: @@ -72,7 +76,6 @@ components: - type - identifier - caption - - debitorUuid - config additionalProperties: false diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index 57f8b866..950d01b0 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -24,13 +24,17 @@ create table if not exists hs_hosting_asset ( uuid uuid unique references RbacObject (uuid), version int not null default 0, - bookingItemUuid uuid not null references hs_booking_item(uuid), + bookingItemUuid uuid null references hs_booking_item(uuid), type HsHostingAssetType not null, parentAssetUuid uuid null references hs_hosting_asset(uuid), identifier varchar(80) not null, caption varchar(80) not null, - config jsonb not null + config jsonb not null, + + constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null) ); + + --// diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index 4924f25e..2495f1ea 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql @@ -39,8 +39,6 @@ begin SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer; SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem; - assert newBookingItem.uuid is not null, format('newBookingItem must not be null for NEW.bookingItemUuid = %s', NEW.bookingItemUuid); - perform createRoleWithGrants( hsHostingAssetOWNER(NEW), diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 0cde4075..0d7c8dc5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.UUID; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -113,7 +114,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/hosting/assets?type=" + HsHostingAssetType.MANAGED_SERVER) + .get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -159,7 +160,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup class AddServer { @Test - void globalAdmin_canAddAsset() { + void globalAdmin_canAddBookedAsset() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); @@ -173,7 +174,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "bookingItemUuid": "%s", "type": "MANAGED_SERVER", "identifier": "vm1400", - "caption": "some new CloudServer", + "caption": "some new ManagedServer", "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """.formatted(givenBookingItem.getUuid())) @@ -187,7 +188,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { "type": "MANAGED_SERVER", "identifier": "vm1400", - "caption": "some new CloudServer", + "caption": "some new ManagedServer", "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """)) @@ -200,6 +201,48 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(newUserUuid).isNotNull(); } + @Test + void parentAssetAgent_canAddSubAsset() { + + context.define("superuser-alex@hostsharing.net"); + final var givenParentAsset = givenParentAsset("First", MANAGED_SERVER); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "person-FirbySusan@example.com") + .contentType(ContentType.JSON) + .body(""" + { + "parentAssetUuid": "%s", + "type": "MANAGED_WEBSPACE", + "identifier": "xyz00", + "caption": "some new ManagedWebspace in client's ManagedServer", + "config": { "SSD": 100, "Traffic": 250 } + } + """.formatted(givenParentAsset.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "MANAGED_WEBSPACE", + "identifier": "xyz00", + "caption": "some new ManagedWebspace in client's ManagedServer", + "config": { "SSD": 100, "Traffic": 250 } + } + """)) + .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 + final var newUserUuid = UUID.fromString( + location.substring(location.lastIndexOf('/') + 1)); + assertThat(newUserUuid).isNotNull(); + } + @Test void additionalValidationsArePerformend_whenAddingAsset() { @@ -215,7 +258,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "bookingItemUuid": "%s", "type": "MANAGED_SERVER", "identifier": "vm1400", - "caption": "some new CloudServer", + "caption": "some new ManagedServer", "config": { "CPUs": 0, "extra": 42 } } """.formatted(givenBookingItem.getUuid())) @@ -235,7 +278,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested - class GetASset { + class GetAsset { @Test void globalAdmin_canGetArbitraryAsset() { @@ -412,6 +455,12 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .findAny().orElseThrow(); } + HsHostingAssetEntity givenParentAsset(final String debitorName, final HsHostingAssetType assetType) { + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow(); + final var givenAsset = assetRepo.findAllByCriteria(givenDebitor.getUuid(), null, assetType).stream().findAny().orElseThrow(); + return givenAsset; + } + private HsHostingAssetEntity givenSomeTemporaryAssetForDebitorNumber(final String identifierSuffix, final Map.Entry resources) { return jpaAttempt.transacted(() -> {