From a93c097f6487130ea5f13b5b672c36ea5ccb59f1 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael.hoennig@hostsharing.net>
Date: Fri, 3 May 2024 10:28:03 +0200
Subject: [PATCH] list hosting-assets with debitor, parent and type
 query-parameters (#52)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/52
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
---
 .../asset/HsHostingAssetController.java       |  9 ++--
 .../hosting/asset/HsHostingAssetEntity.java   |  8 ++-
 .../asset/HsHostingAssetRepository.java       | 12 +++--
 .../hs/hosting/asset/HsHostingAssetType.java  |  9 ++++
 .../hs-hosting/hs-hosting-assets.yaml         | 19 +++++--
 .../db/changelog/1-rbac/1050-rbac-base.sql    |  2 +-
 ...sHostingAssetControllerAcceptanceTest.java | 54 ++++++++++++++++++-
 .../asset/HsHostingAssetEntityUnitTest.java   |  4 +-
 ...HostingAssetRepositoryIntegrationTest.java | 43 ++++++++++-----
 9 files changed, 127 insertions(+), 33 deletions(-)

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 78606936..62a62b34 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
@@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.context.Context;
 import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetInsertResource;
 import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource;
 import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
+import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
 import net.hostsharing.hsadminng.mapper.KeyValueMap;
 import net.hostsharing.hsadminng.mapper.Mapper;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -33,13 +34,15 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
 
     @Override
     @Transactional(readOnly = true)
-    public ResponseEntity<List<HsHostingAssetResource>> listAssetsByDebitorUuid(
+    public ResponseEntity<List<HsHostingAssetResource>> listAssets(
             final String currentUser,
             final String assumedRoles,
-            final UUID debitorUuid) {
+            final UUID debitorUuid,
+            final UUID parentAssetUuid,
+            final HsHostingAssetTypeResource type) {
         context.define(currentUser, assumedRoles);
 
-        final var entities = assetRepo.findAllByDebitorUuid(debitorUuid);
+        final var entities = assetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type));
 
         final var resources = mapper.mapList(entities, HsHostingAssetResource.class);
         return ResponseEntity.ok(resources);
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 462ccd2c..52466e82 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
@@ -32,7 +32,6 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
-import static java.util.Optional.ofNullable;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@@ -65,11 +64,11 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 public class HsHostingAssetEntity implements Stringifyable, RbacObject {
 
     private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
-            .withProp(HsHostingAssetEntity::getBookingItem)
             .withProp(HsHostingAssetEntity::getType)
-            .withProp(HsHostingAssetEntity::getParentAsset)
             .withProp(HsHostingAssetEntity::getIdentifier)
             .withProp(HsHostingAssetEntity::getCaption)
+            .withProp(HsHostingAssetEntity::getParentAsset)
+            .withProp(HsHostingAssetEntity::getBookingItem)
             .withProp(HsHostingAssetEntity::getConfig)
             .quotedValues(false);
 
@@ -122,8 +121,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
 
     @Override
     public String toShortString() {
-        return ofNullable(bookingItem).map(HsBookingItemEntity::toShortString).orElse("D-???????:?") +
-                ":" + identifier;
+        return type + ":" + identifier;
     }
 
     public static RbacView rbac() {
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java
index 67808097..4926c673 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java
@@ -7,16 +7,22 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 
+
 public interface HsHostingAssetRepository extends Repository<HsHostingAssetEntity, UUID> {
 
     List<HsHostingAssetEntity> findAll();
     Optional<HsHostingAssetEntity> findByUuid(final UUID serverUuid);
 
     @Query("""
-        SELECT s FROM HsHostingAssetEntity s
-            WHERE s.bookingItem.debitor.uuid = :debitorUuid
+        SELECT asset FROM HsHostingAssetEntity asset
+            WHERE (:debitorUuid IS NULL OR asset.bookingItem.debitor.uuid = :debitorUuid)
+              AND (:parentAssetUuid IS NULL OR asset.parentAsset.uuid = :parentAssetUuid)
+              AND (:type IS NULL OR :type = CAST(asset.type AS String))
     """)
-    List<HsHostingAssetEntity> findAllByDebitorUuid(final UUID debitorUuid);
+    List<HsHostingAssetEntity> findAllByCriteriaImpl(UUID debitorUuid, UUID parentAssetUuid, String type);
+    default List<HsHostingAssetEntity> findAllByCriteria(final UUID debitorUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
+        return findAllByCriteriaImpl(debitorUuid, parentAssetUuid, HsHostingAssetType.asString(type));
+    }
 
     HsHostingAssetEntity save(HsHostingAssetEntity current);
 
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
index 9e99a8c5..f4040046 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
@@ -1,5 +1,6 @@
 package net.hostsharing.hsadminng.hs.hosting.asset;
 
+
 public enum HsHostingAssetType {
     CLOUD_SERVER, // named e.g. vm1234
     MANAGED_SERVER, // named e.g. vm1234
@@ -25,4 +26,12 @@ public enum HsHostingAssetType {
     HsHostingAssetType() {
         this(null);
     }
+
+    public static <T extends Enum<?>> HsHostingAssetType of(final T value) {
+        return value == null ? null : valueOf(value.name());
+    }
+
+    static String asString(final HsHostingAssetType type) {
+        return type == null ? null : type.name();
+    }
 }
diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml
index d74766ed..8b81ecc7 100644
--- a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml
+++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml
@@ -1,18 +1,29 @@
 get:
-    summary: Returns a list of all hosting assets for a specified debitor.
-    description: Returns the list of all hosting assets for a debitor which are visible to the current user or any of it's assumed roles.
+    summary: Returns a filtered list of all hosting assets.
+    description: Returns the list of all hosting assets which match the given filters and are visible to the current user or any of it's assumed roles.
     tags:
         - hs-hosting-assets
-    operationId: listAssetsByDebitorUuid
+    operationId: listAssets
     parameters:
         - $ref: 'auth.yaml#/components/parameters/currentUser'
         - $ref: 'auth.yaml#/components/parameters/assumedRoles'
         - name: debitorUuid
           in: query
-          required: true
+          required: false
           schema:
               type: string
               format: uuid
+        - name: parentAssetUuid
+          in: query
+          required: false
+          schema:
+              type: string
+              format: uuid
+        - name: type
+          in: query
+          required: false
+          schema:
+              $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType'
           description: The UUID of the debitor, whose hosting assets are to be listed.
     responses:
         "200":
diff --git a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
index cf49baee..6de59816 100644
--- a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
+++ b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
@@ -97,7 +97,7 @@ $$;
 create table RbacObject
 (
     uuid        uuid primary key default uuid_generate_v4(),
-    serialId    serial, -- TODO: we might want to remove this once test data deletion works properly
+    serialId    serial, -- TODO.perf: only needed for reverse deletion of temp test data
     objectTable varchar(64) not null,
     unique (objectTable, uuid)
 );
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 d2c73b7c..26d1b763 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
@@ -101,6 +101,58 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
                     """));
                 // @formatter:on
         }
+
+        @Test
+        void globalAdmin_canViewAllAssetsByType() {
+
+            // given
+            context("superuser-alex@hostsharing.net");
+
+            RestAssured // @formatter:off
+                    .given()
+                    .header("current-user", "superuser-alex@hostsharing.net")
+                    .port(port)
+                    .when()
+                    .get("http://localhost/api/hs/hosting/assets?type=" + HsHostingAssetType.MANAGED_SERVER)
+                    .then().log().all().assertThat()
+                    .statusCode(200)
+                    .contentType("application/json")
+                    .body("", lenientlyEquals("""
+                    [
+                        {
+                            "type": "MANAGED_SERVER",
+                            "identifier": "vm1011",
+                            "caption": "some ManagedServer",
+                            "config": {
+                                "CPU": 2,
+                                "SDD": 512,
+                                "extra": 42
+                            }
+                        },
+                        {
+                            "type": "MANAGED_SERVER",
+                            "identifier": "vm1013",
+                            "caption": "some ManagedServer",
+                            "config": {
+                                "CPU": 2,
+                                "SDD": 512,
+                                "extra": 42
+                            }
+                        },
+                        {
+                            "type": "MANAGED_SERVER",
+                            "identifier": "vm1012",
+                            "caption": "some ManagedServer",
+                            "config": {
+                                "CPU": 2,
+                                "SDD": 512,
+                                "extra": 42
+                            }
+                        }
+                    ]
+                    """));
+            // @formatter:on
+        }
     }
 
     @Nested
@@ -274,7 +326,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
             context.define("superuser-alex@hostsharing.net");
             assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get()
                     .matches(asset -> {
-                        assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(D-1000111:some CloudServer, CLOUD_SERVER, vm2001, some test-asset, { CPU: 4, SSD: 4096, something: 1 })");
+                        assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(CLOUD_SERVER, vm2001, some test-asset, D-1000111:some CloudServer, { CPU: 4, SSD: 4096, something: 1 })");
                         return true;
                     });
         }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java
index 4a878bf7..2f0fc00a 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java
@@ -37,13 +37,13 @@ class HsHostingAssetEntityUnitTest {
         final var result = givenServer.toString();
 
         assertThat(result).isEqualTo(
-                "HsHostingAssetEntity(D-1000100:test booking item, MANAGED_WEBSPACE, D-1000100:test booking item:vm1234, xyz00, some managed webspace, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
+                "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1000100:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
     }
 
     @Test
     void toShortStringContainsOnlyMemberNumberAndCaption() {
         final var result = givenServer.toShortString();
 
-        assertThat(result).isEqualTo("D-1000100:test booking item:xyz00");
+        assertThat(result).isEqualTo("MANAGED_WEBSPACE:xyz00");
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
index 2ce1eff6..83a07599 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import static java.util.Map.entry;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
 import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
 import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
@@ -77,7 +78,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
                         .bookingItem(givenManagedServer.getBookingItem())
                         .parentAsset(givenManagedServer)
                         .caption("some new managed webspace")
-                        .type(HsHostingAssetType.MANAGED_WEBSPACE)
+                        .type(MANAGED_WEBSPACE)
                         .identifier("xyz90")
                         .build();
                 return toCleanup(assetRepo.save(newAsset));
@@ -151,21 +152,19 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
     class FindByDebitorUuid {
 
         @Test
-        public void globalAdmin_withoutAssumedRole_canViewAllAssetsOfArbitraryDebitor() {
+        public void globalAdmin_withoutAssumedRole_canViewArbitraryAssetsOfAllDebitors() {
             // given
             context("superuser-alex@hostsharing.net");
-            final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream()
-                    .findAny().orElseThrow().getUuid();
 
             // when
-            final var result = assetRepo.findAllByDebitorUuid(debitorUuid);
+            final var result = assetRepo.findAllByCriteria(null, null, MANAGED_WEBSPACE);
 
             // then
             allTheseServersAreReturned(
                     result,
-                    "HsHostingAssetEntity(D-1000212:some ManagedServer, MANAGED_WEBSPACE, D-1000212:some PrivateCloud:vm1012, bbb01, some Webspace, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })",
-                    "HsHostingAssetEntity(D-1000212:some PrivateCloud, MANAGED_SERVER, vm1012, some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })",
-                    "HsHostingAssetEntity(D-1000212:some PrivateCloud, CLOUD_SERVER, vm2012, another CloudServer, { CPU: 2, HDD: 1024, extra: 42 })");
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, bbb01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })",
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })",
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, ccc01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })");
         }
 
         @Test
@@ -175,15 +174,32 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
             final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid();
 
             // when:
-            final var result = assetRepo.findAllByDebitorUuid(debitorUuid);
+            final var result = assetRepo.findAllByCriteria(debitorUuid, null, null);
 
             // then:
             exactlyTheseAssetsAreReturned(
                     result,
-                    "HsHostingAssetEntity(D-1000111:some ManagedServer, MANAGED_WEBSPACE, D-1000111:some PrivateCloud:vm1011, aaa01, some Webspace, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })",
-                    "HsHostingAssetEntity(D-1000111:some PrivateCloud, MANAGED_SERVER, vm1011, some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })",
-                    "HsHostingAssetEntity(D-1000111:some PrivateCloud, CLOUD_SERVER, vm2011, another CloudServer, { CPU: 2, HDD: 1024, extra: 42 })");
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })",
+                    "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:some PrivateCloud, { CPU: 2, SDD: 512, extra: 42 })",
+                    "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:some PrivateCloud, { CPU: 2, HDD: 1024, extra: 42 })");
         }
+
+        @Test
+        public void normalUser_canFilterAssetsRelatedToParentAsset() {
+            // given
+            context("superuser-alex@hostsharing.net");
+            final var parentAssetUuid = assetRepo.findAllByCriteria(null, null, MANAGED_SERVER).stream()
+                    .findAny().orElseThrow().getUuid();
+
+            // when
+            final var result = assetRepo.findAllByCriteria(null, parentAssetUuid, null);
+
+            // then
+            allTheseServersAreReturned(
+                    result,
+                    "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })");
+        }
+
     }
 
     @Nested
@@ -356,8 +372,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
 
     HsHostingAssetEntity givenManagedServer(final String debitorName, final HsHostingAssetType type) {
         final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow();
-        return assetRepo.findAllByDebitorUuid(givenDebitor.getUuid()).stream()
-                .filter(i -> i.getType().equals(type))
+        return assetRepo.findAllByCriteria(givenDebitor.getUuid(), null, type).stream()
                 .findAny().orElseThrow();
     }