From 85f2a19eafbff6f05e65bd033b4ae610f1e706c8 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 12 Apr 2024 16:09:43 +0200 Subject: [PATCH 01/21] introduce booking module, HsBookingItemControllerAcceptanceTest still failing --- build.gradle | 18 +- .../booking/item/HsBookingItemController.java | 131 ++++++ .../hs/booking/item/HsBookingItemEntity.java | 154 +++++++ .../item/HsBookingItemEntityPatcher.java | 53 +++ .../booking/item/HsBookingItemRepository.java | 21 + .../hs-booking/api-mappings.yaml | 18 + .../api-definition/hs-booking/auth.yaml | 20 + .../hs-booking/error-responses.yaml | 40 ++ .../hs-booking/hs-booking-item-schemas.yaml | 81 ++++ .../hs-booking-items-with-uuid.yaml | 83 ++++ .../hs-booking/hs-booking-items.yaml | 58 +++ .../api-definition/hs-booking/hs-booking.yaml | 17 + .../5070-hs-office-sepamandate.sql | 2 +- .../601-booking-item/6010-hs-booking-item.sql | 24 ++ .../6013-hs-booking-item-rbac.md | 285 +++++++++++++ .../6013-hs-booking-item-rbac.sql | 194 +++++++++ .../6018-hs-booking-item-test-data.sql | 51 +++ .../db/changelog/db.changelog-master.yaml | 6 + ...HsBookingItemControllerAcceptanceTest.java | 396 ++++++++++++++++++ .../HsBookingItemEntityPatcherUnitTest.java | 104 +++++ .../item/HsBookingItemEntityUnitTest.java | 56 +++ ...sBookingItemRepositoryIntegrationTest.java | 341 +++++++++++++++ 22 files changed, 2150 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java create mode 100644 src/main/resources/api-definition/hs-booking/api-mappings.yaml create mode 100644 src/main/resources/api-definition/hs-booking/auth.yaml create mode 100644 src/main/resources/api-definition/hs-booking/error-responses.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-items.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking.yaml create mode 100644 src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql create mode 100644 src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md create mode 100644 src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql create mode 100644 src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java diff --git a/build.gradle b/build.gradle index 90db8970..32f42cd6 100644 --- a/build.gradle +++ b/build.gradle @@ -140,7 +140,7 @@ openapiProcessor { showWarnings true openApiNullable true } - springHs { + springHsOffice { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2024.2' apiPath "$projectDir/src/main/resources/api-definition/hs-office/hs-office.yaml" @@ -149,11 +149,25 @@ openapiProcessor { showWarnings true openApiNullable true } + springHsBooking { + processorName 'spring' + processor 'io.openapiprocessor:openapi-processor-spring:2022.5' + apiPath "$projectDir/src/main/resources/api-definition/hs-booking/hs-booking.yaml" + mapping "$projectDir/src/main/resources/api-definition/hs-booking/api-mappings.yaml" + targetDir "$buildDir/generated/sources/openapi-javax" + showWarnings true + openApiNullable true + } } sourceSets.main.java.srcDir 'build/generated/sources/openapi' abstract class ProcessSpring extends DefaultTask {} tasks.register('processSpring', ProcessSpring) -['processSpringRoot', 'processSpringRbac', 'processSpringTest', 'processSpringHs'].each { +['processSpringRoot', + 'processSpringRbac', + 'processSpringTest', + 'processSpringHsOffice', + 'processSpringHsBooking' +].each { project.tasks.processSpring.dependsOn it } project.tasks.processResources.dependsOn processSpring diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java new file mode 100644 index 00000000..4aeb8172 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -0,0 +1,131 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.context.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; + +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; + +@RestController +public class HsBookingItemController implements HsBookingItemsApi { + + @Autowired + private Context context; + + @Autowired + private Mapper mapper; + + @Autowired + private HsBookingItemRepository bookingItemRepo; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listBookingItemsByDebitorUuid( + final String currentUser, + final String assumedRoles, + final UUID debitorUuid) { + context.define(currentUser, assumedRoles); + + final var entities = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + + final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addBookingItem( + final String currentUser, + final String assumedRoles, + final HsBookingItemInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); + + final var saved = bookingItemRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/office/bookingItems/{id}") + .buildAndExpand(saved.getUuid()) + .toUri(); + final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.created(uri).body(mapped); + } + + @Override + @Transactional(readOnly = true) + public ResponseEntity getBookingItemByUuid( + final String currentUser, + final String assumedRoles, + final UUID bookingItemUuid) { + + context.define(currentUser, assumedRoles); + + final var result = bookingItemRepo.findByUuid(bookingItemUuid); + if (result.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(mapper.map(result.get(), HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)); + } + + @Override + @Transactional + public ResponseEntity deleteBookingIemByUuid( + final String currentUser, + final String assumedRoles, + final UUID bookingItemUuid) { + context.define(currentUser, assumedRoles); + + final var result = bookingItemRepo.deleteByUuid(bookingItemUuid); + if (result == 0) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.noContent().build(); + } + + @Override + @Transactional + public ResponseEntity patchBookingItem( + final String currentUser, + final String assumedRoles, + final UUID bookingItemUuid, + final HsBookingItemPatchResource body) { + + context.define(currentUser, assumedRoles); + + final var current = bookingItemRepo.findByUuid(bookingItemUuid).orElseThrow(); + + new HsBookingItemEntityPatcher(current).apply(body); + + final var saved = bookingItemRepo.save(current); + final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.ok(mapped); + } + + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + resource.setValidFrom(entity.getValidity().lower()); + if (entity.getValidity().hasUpperBound()) { + resource.setValidTo(entity.getValidity().upper().minusDays(1)); + } + resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber()); + }; + + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); + }; +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java new file mode 100644 index 00000000..c5edf1b1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -0,0 +1,154 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType; +import io.hypersistence.utils.hibernate.type.range.Range; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; +import net.hostsharing.hsadminng.stringify.Stringify; +import net.hostsharing.hsadminng.stringify.Stringifyable; +import org.hibernate.annotations.Type; + +import jakarta.persistence.*; +import java.io.IOException; +import java.time.LocalDate; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.function.BinaryOperator; + +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toMap; +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; +import static net.hostsharing.hsadminng.stringify.Stringify.stringify; + +@Builder +@Entity +@Table(name = "hs_booking_item_rv") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class HsBookingItemEntity implements Stringifyable, RbacObject { + + private static Stringify stringify = stringify(HsBookingItemEntity.class) + .withProp(e -> e.getDebitor().toShortString()) + .withProp(HsBookingItemEntity::getCaption) + .withProp(e -> e.getValidity().asString()) + .withProp(HsBookingItemEntity::getResources) + .quotedValues(false); + + @Id + @GeneratedValue + private UUID uuid; + + @Version + private int version; + + @Column(name = "caption") + private String caption; + + @ManyToOne(optional = false) + @JoinColumn(name = "debitoruuid") + private HsOfficeDebitorEntity debitor; + + @Builder.Default + @Type(PostgreSQLRangeType.class) + @Column(name = "validity", columnDefinition = "daterange") + private Range validity = Range.emptyRange(LocalDate.class); + + @Builder.Default + @Type(JsonType.class) + @Column(columnDefinition = "resources") + private Map resources = new TreeMap<>(); + + public Map getResources() { + return resources.entrySet().stream() + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, HsBookingItemEntity::thereIsOnlyOneValuePerKey, TreeMap::new)); + } + + public void setValidFrom(final LocalDate validFrom) { + setValidity(toPostgresDateRange(validFrom, getValidTo())); + } + + public void setValidTo(final LocalDate validTo) { + setValidity(toPostgresDateRange(getValidFrom(), validTo)); + } + + public LocalDate getValidFrom() { + return lowerInclusiveFromPostgresDateRange(getValidity()); + } + + public LocalDate getValidTo() { + return upperInclusiveFromPostgresDateRange(getValidity()); + } + + + @Override + public String toString() { + return stringify.apply(this); + } + + @Override + public String toShortString() { + return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") + + ":" + caption; + } + + private static BinaryOperator thereIsOnlyOneValuePerKey(Object o, Object o1) { + return (a, b) -> a; + } + + public static RbacView rbac() { + return rbacViewFor("bookingItem", HsBookingItemEntity.class) + .withIdentityView(SQL.projection("caption")) // FIXME: use memberNumber:caption + .withUpdatableColumns("version", "validity", "resources") + + .importEntityAlias("debitor", HsOfficeDebitorEntity.class, + dependsOnColumn("debitorUuid"), + directlyFetchedByDependsOnColumn(), + NOT_NULL) + + .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, + dependsOnColumn("debitorUuid"), + fetchedBySql(""" + SELECT ${columns} + FROM hs_office_relation debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = ${REF}.debitorUuid + """), + NOT_NULL) + .toRole("debitorRel", ADMIN).grantPermission(INSERT) + .toRole("global", ADMIN).grantPermission(DELETE) + + .createRole(OWNER, (with) -> { + with.incomingSuperRole("debitorRel", AGENT); + with.permission(UPDATE); + }) + .createSubRole(ADMIN) + .createSubRole(TENANT, (with) -> { + with.outgoingSubRole("debitorRel", TENANT); + with.permission(SELECT); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("6-hs-booking/601-booking-item/6013-hs-booking-item-rbac"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java new file mode 100644 index 00000000..865e16c8 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -0,0 +1,53 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; +import net.hostsharing.hsadminng.mapper.EntityPatcher; +import net.hostsharing.hsadminng.mapper.OptionalFromJson; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; + +public class HsBookingItemEntityPatcher implements EntityPatcher { + + private final HsBookingItemEntity entity; + + public HsBookingItemEntityPatcher(final HsBookingItemEntity entity) { + this.entity = entity; + } + + @Override + public void apply(final HsBookingItemPatchResource resource) { + OptionalFromJson.of(resource.getCaption()) + .ifPresent(entity::setCaption); + Optional.ofNullable(resource.getResources()) + .ifPresent(r -> setResources(entity, r)); + OptionalFromJson.of(resource.getValidFrom()) + .ifPresent(entity::setValidFrom); + OptionalFromJson.of(resource.getValidTo()) + .ifPresent(entity::setValidTo); + } + + private static void setResources( + final HsBookingItemEntity entity, + final ArbitraryBookingResourcesJsonResource arbitraryBookingResourcesJsonResource) { + entity.getResources().putAll(objectToMap(arbitraryBookingResourcesJsonResource)); + } + + static Map objectToMap(final Object obj) { + final var map = stream(obj.getClass().getDeclaredFields()) + .map(field -> { + try { + field.setAccessible(true); + return Map.entry(field.getName(), field.get(obj)); + } catch (final IllegalAccessException exc) { + throw new RuntimeException(exc); + } + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return map; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java new file mode 100644 index 00000000..6d9bd683 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java @@ -0,0 +1,21 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface HsBookingItemRepository extends Repository { + + List findAll(); + Optional findByUuid(final UUID bookingItemUuid); + + List findAllByDebitorUuid(final UUID bookingItemUuid); + + HsBookingItemEntity save(HsBookingItemEntity current); + + int deleteByUuid(final UUID uuid); + + long count(); +} diff --git a/src/main/resources/api-definition/hs-booking/api-mappings.yaml b/src/main/resources/api-definition/hs-booking/api-mappings.yaml new file mode 100644 index 00000000..9faac586 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/api-mappings.yaml @@ -0,0 +1,18 @@ +openapi-processor-mapping: v2 + +options: + package-name: net.hostsharing.hsadminng.hs.booking.generated.api.v1 + model-name-suffix: Resource + bean-validation: true + +map: + result: org.springframework.http.ResponseEntity + + types: + - type: array => java.util.List + - type: string:uuid => java.util.UUID + - type: string:format => java.lang.String + + paths: + /api/hs/booking/items/{itemUUID}: + null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-booking/auth.yaml b/src/main/resources/api-definition/hs-booking/auth.yaml new file mode 100644 index 00000000..65d491fb --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/auth.yaml @@ -0,0 +1,20 @@ + +components: + + parameters: + + currentUser: + name: current-user + in: header + required: true + schema: + type: string + description: Identifying name of the currently logged in user. + + assumedRoles: + name: assumed-roles + in: header + required: false + schema: + type: string + description: Semicolon-separated list of roles to assume. The current user needs to have the right to assume these roles. diff --git a/src/main/resources/api-definition/hs-booking/error-responses.yaml b/src/main/resources/api-definition/hs-booking/error-responses.yaml new file mode 100644 index 00000000..83ca3dfb --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/error-responses.yaml @@ -0,0 +1,40 @@ +components: + + responses: + NotFound: + description: The specified was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: The current user is unknown or not authorized. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: The current user or none of the assumed or roles is granted access to the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Conflict: + description: The request could not be completed due to a conflict with the current state of the target resource. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + schemas: + + Error: + type: object + properties: + code: + type: string + message: + type: string + required: + - code + - message diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml new file mode 100644 index 00000000..1b0fb77c --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -0,0 +1,81 @@ + +components: + + schemas: + + HsBookingItem: + type: object + properties: + uuid: + type: string + format: uuid + debitor: + $ref: '../hs-office/hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor' + caption: + type: string + validFrom: + type: string + format: date + validTo: + type: string + format: date + resources: + $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + required: + - uuid + - debitor + - validFrom + - validTo + - resources + + HsBookingItemPatch: + type: object + properties: + caption: + type: string + nullable: true + validFrom: + type: string + format: date + nullable: true + validTo: + type: string + format: date + nullable: true + resources: + $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + additionalProperties: false + + HsBookingItemInsert: + type: object + properties: + debitorUuid: + type: string + format: uuid + nullable: false + caption: + type: string + minLength: 3 + maxLength: + nullable: false + validFrom: + type: string + format: date + nullable: false + validTo: + type: string + format: date + nullable: true + resources: + $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + required: + - caption + - debitorUuid + - validFrom + - resources + additionalProperties: false + + ArbitraryBookingResourcesJson: + type: object + description: An object containing arbitrary JSON + additionalProperties: true diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml new file mode 100644 index 00000000..955e45b8 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml @@ -0,0 +1,83 @@ +get: + tags: + - hs-booking-items + description: 'Fetch a single booking item its uuid, if visible for the current subject.' + operationId: getBookingItemByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: bookingItemUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the booking item to fetch. + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +patch: + tags: + - hs-booking-items + description: 'Updates a single booking item identified by its uuid, if permitted for the current subject.' + operationId: patchBookingItem + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: bookingItemUUID + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + 'application/json': + schema: + $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemPatch' + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +delete: + tags: + - hs-booking-items + description: 'Delete a single booking item identified by its uuid, if permitted for the current subject.' + operationId: deleteBookingIemByUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: bookingItemUUID + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the booking item to delete. + responses: + "204": + description: No Content + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "404": + $ref: './error-responses.yaml#/components/responses/NotFound' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml new file mode 100644 index 00000000..4fd8b650 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml @@ -0,0 +1,58 @@ +get: + summary: Returns a list of (optionally filtered) booking items. + description: Returns the list of (optionally filtered) booking items which are visible to the current user or any of it's assumed roles. + tags: + - hs-booking-items + operationId: listBookingItemsByDebitorUuid + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + - name: debitorUuid + in: query + required: false + schema: + type: string + format: uuid + description: The UUID of the debitor, whose booking items are to be listed. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new booking item. + tags: + - hs-booking-items + operationId: addBookingItem + parameters: + - $ref: './auth.yaml#/components/parameters/currentUser' + - $ref: './auth.yaml#/components/parameters/assumedRoles' + requestBody: + description: A JSON object describing the new booking item. + required: true + content: + application/json: + schema: + $ref: '/hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemInsert' + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + "401": + $ref: './error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: './error-responses.yaml#/components/responses/Forbidden' + "409": + $ref: './error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking.yaml b/src/main/resources/api-definition/hs-booking/hs-booking.yaml new file mode 100644 index 00000000..e2311b85 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking.yaml @@ -0,0 +1,17 @@ +openapi: 3.0.3 +info: + title: Hostsharing hsadmin-ng API + version: v0 +servers: + - url: http://localhost:8080 + description: Local development default URL. + +paths: + + # Items + + /api/hs/booking/items: + $ref: "./hs-booking-items.yaml" + + /api/hs/booking/items/{itemUUID}: + $ref: "./hs-booking-items-with-uuid.yaml" diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql index 841f429f..c2ffd86d 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql @@ -7,7 +7,7 @@ create table if not exists hs_office_sepamandate ( uuid uuid unique references RbacObject (uuid) initially deferred, - version int not null default 0, + version int not null default 0, debitorUuid uuid not null references hs_office_debitor(uuid), bankAccountUuid uuid not null references hs_office_bankaccount(uuid), reference varchar(96) not null, diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql new file mode 100644 index 00000000..ec671494 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql @@ -0,0 +1,24 @@ +--liquibase formatted sql + +-- ============================================================================ +--changeset booking-item-MAIN-TABLE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create table if not exists hs_booking_item +( + uuid uuid unique references RbacObject (uuid), + version int not null default 0, + debitorUuid uuid not null references hs_office_debitor(uuid), + caption varchar(80) not null, + validity daterange not null, + resources jsonb not null +); +--// + + +-- ============================================================================ +--changeset hs-booking-item-MAIN-TABLE-JOURNAL:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call create_journal('hs_booking_item'); +--// diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md new file mode 100644 index 00000000..1acb787d --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md @@ -0,0 +1,285 @@ +### rbac bookingItem + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph debitor.debitorRel.anchorPerson["`**debitor.debitorRel.anchorPerson**`"] + direction TB + style debitor.debitorRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.debitorRel.anchorPerson:roles[ ] + style debitor.debitorRel.anchorPerson:roles fill:#99bcdb,stroke:white + + role:debitor.debitorRel.anchorPerson:OWNER[[debitor.debitorRel.anchorPerson:OWNER]] + role:debitor.debitorRel.anchorPerson:ADMIN[[debitor.debitorRel.anchorPerson:ADMIN]] + role:debitor.debitorRel.anchorPerson:REFERRER[[debitor.debitorRel.anchorPerson:REFERRER]] + end +end + +subgraph debitor.debitorRel.holderPerson["`**debitor.debitorRel.holderPerson**`"] + direction TB + style debitor.debitorRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.debitorRel.holderPerson:roles[ ] + style debitor.debitorRel.holderPerson:roles fill:#99bcdb,stroke:white + + role:debitor.debitorRel.holderPerson:OWNER[[debitor.debitorRel.holderPerson:OWNER]] + role:debitor.debitorRel.holderPerson:ADMIN[[debitor.debitorRel.holderPerson:ADMIN]] + role:debitor.debitorRel.holderPerson:REFERRER[[debitor.debitorRel.holderPerson:REFERRER]] + end +end + +subgraph debitorRel.anchorPerson["`**debitorRel.anchorPerson**`"] + direction TB + style debitorRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel.anchorPerson:roles[ ] + style debitorRel.anchorPerson:roles fill:#99bcdb,stroke:white + + role:debitorRel.anchorPerson:OWNER[[debitorRel.anchorPerson:OWNER]] + role:debitorRel.anchorPerson:ADMIN[[debitorRel.anchorPerson:ADMIN]] + role:debitorRel.anchorPerson:REFERRER[[debitorRel.anchorPerson:REFERRER]] + end +end + +subgraph debitorRel.holderPerson["`**debitorRel.holderPerson**`"] + direction TB + style debitorRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel.holderPerson:roles[ ] + style debitorRel.holderPerson:roles fill:#99bcdb,stroke:white + + role:debitorRel.holderPerson:OWNER[[debitorRel.holderPerson:OWNER]] + role:debitorRel.holderPerson:ADMIN[[debitorRel.holderPerson:ADMIN]] + role:debitorRel.holderPerson:REFERRER[[debitorRel.holderPerson:REFERRER]] + end +end + +subgraph debitor.debitorRel["`**debitor.debitorRel**`"] + direction TB + style debitor.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.debitorRel:roles[ ] + style debitor.debitorRel:roles fill:#99bcdb,stroke:white + + role:debitor.debitorRel:OWNER[[debitor.debitorRel:OWNER]] + role:debitor.debitorRel:ADMIN[[debitor.debitorRel:ADMIN]] + role:debitor.debitorRel:AGENT[[debitor.debitorRel:AGENT]] + role:debitor.debitorRel:TENANT[[debitor.debitorRel:TENANT]] + end +end + +subgraph debitor.partnerRel["`**debitor.partnerRel**`"] + direction TB + style debitor.partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.partnerRel:roles[ ] + style debitor.partnerRel:roles fill:#99bcdb,stroke:white + + role:debitor.partnerRel:OWNER[[debitor.partnerRel:OWNER]] + role:debitor.partnerRel:ADMIN[[debitor.partnerRel:ADMIN]] + role:debitor.partnerRel:AGENT[[debitor.partnerRel:AGENT]] + role:debitor.partnerRel:TENANT[[debitor.partnerRel:TENANT]] + end +end + +subgraph bookingItem["`**bookingItem**`"] + direction TB + style bookingItem fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph bookingItem:roles[ ] + style bookingItem:roles fill:#dd4901,stroke:white + + role:bookingItem:OWNER[[bookingItem:OWNER]] + role:bookingItem:ADMIN[[bookingItem:ADMIN]] + role:bookingItem:TENANT[[bookingItem:TENANT]] + end + + subgraph bookingItem:permissions[ ] + style bookingItem:permissions fill:#dd4901,stroke:white + + perm:bookingItem:INSERT{{bookingItem:INSERT}} + perm:bookingItem:DELETE{{bookingItem:DELETE}} + perm:bookingItem:UPDATE{{bookingItem:UPDATE}} + perm:bookingItem:SELECT{{bookingItem:SELECT}} + end +end + +subgraph debitor.partnerRel.contact["`**debitor.partnerRel.contact**`"] + direction TB + style debitor.partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.partnerRel.contact:roles[ ] + style debitor.partnerRel.contact:roles fill:#99bcdb,stroke:white + + role:debitor.partnerRel.contact:OWNER[[debitor.partnerRel.contact:OWNER]] + role:debitor.partnerRel.contact:ADMIN[[debitor.partnerRel.contact:ADMIN]] + role:debitor.partnerRel.contact:REFERRER[[debitor.partnerRel.contact:REFERRER]] + end +end + +subgraph debitor.partnerRel.holderPerson["`**debitor.partnerRel.holderPerson**`"] + direction TB + style debitor.partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.partnerRel.holderPerson:roles[ ] + style debitor.partnerRel.holderPerson:roles fill:#99bcdb,stroke:white + + role:debitor.partnerRel.holderPerson:OWNER[[debitor.partnerRel.holderPerson:OWNER]] + role:debitor.partnerRel.holderPerson:ADMIN[[debitor.partnerRel.holderPerson:ADMIN]] + role:debitor.partnerRel.holderPerson:REFERRER[[debitor.partnerRel.holderPerson:REFERRER]] + end +end + +subgraph debitor["`**debitor**`"] + direction TB + style debitor fill:#99bcdb,stroke:#274d6e,stroke-width:8px +end + +subgraph debitor.refundBankAccount["`**debitor.refundBankAccount**`"] + direction TB + style debitor.refundBankAccount fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.refundBankAccount:roles[ ] + style debitor.refundBankAccount:roles fill:#99bcdb,stroke:white + + role:debitor.refundBankAccount:OWNER[[debitor.refundBankAccount:OWNER]] + role:debitor.refundBankAccount:ADMIN[[debitor.refundBankAccount:ADMIN]] + role:debitor.refundBankAccount:REFERRER[[debitor.refundBankAccount:REFERRER]] + end +end + +subgraph debitor.partnerRel.anchorPerson["`**debitor.partnerRel.anchorPerson**`"] + direction TB + style debitor.partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.partnerRel.anchorPerson:roles[ ] + style debitor.partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white + + role:debitor.partnerRel.anchorPerson:OWNER[[debitor.partnerRel.anchorPerson:OWNER]] + role:debitor.partnerRel.anchorPerson:ADMIN[[debitor.partnerRel.anchorPerson:ADMIN]] + role:debitor.partnerRel.anchorPerson:REFERRER[[debitor.partnerRel.anchorPerson:REFERRER]] + end +end + +subgraph debitorRel.contact["`**debitorRel.contact**`"] + direction TB + style debitorRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel.contact:roles[ ] + style debitorRel.contact:roles fill:#99bcdb,stroke:white + + role:debitorRel.contact:OWNER[[debitorRel.contact:OWNER]] + role:debitorRel.contact:ADMIN[[debitorRel.contact:ADMIN]] + role:debitorRel.contact:REFERRER[[debitorRel.contact:REFERRER]] + end +end + +subgraph debitor.debitorRel.contact["`**debitor.debitorRel.contact**`"] + direction TB + style debitor.debitorRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitor.debitorRel.contact:roles[ ] + style debitor.debitorRel.contact:roles fill:#99bcdb,stroke:white + + role:debitor.debitorRel.contact:OWNER[[debitor.debitorRel.contact:OWNER]] + role:debitor.debitorRel.contact:ADMIN[[debitor.debitorRel.contact:ADMIN]] + role:debitor.debitorRel.contact:REFERRER[[debitor.debitorRel.contact:REFERRER]] + end +end + +subgraph debitorRel["`**debitorRel**`"] + direction TB + style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel:roles[ ] + style debitorRel:roles fill:#99bcdb,stroke:white + + role:debitorRel:OWNER[[debitorRel:OWNER]] + role:debitorRel:ADMIN[[debitorRel:ADMIN]] + role:debitorRel:AGENT[[debitorRel:AGENT]] + role:debitorRel:TENANT[[debitorRel:TENANT]] + end +end + +%% granting roles to roles +role:global:ADMIN -.-> role:debitor.debitorRel.anchorPerson:OWNER +role:debitor.debitorRel.anchorPerson:OWNER -.-> role:debitor.debitorRel.anchorPerson:ADMIN +role:debitor.debitorRel.anchorPerson:ADMIN -.-> role:debitor.debitorRel.anchorPerson:REFERRER +role:global:ADMIN -.-> role:debitor.debitorRel.holderPerson:OWNER +role:debitor.debitorRel.holderPerson:OWNER -.-> role:debitor.debitorRel.holderPerson:ADMIN +role:debitor.debitorRel.holderPerson:ADMIN -.-> role:debitor.debitorRel.holderPerson:REFERRER +role:global:ADMIN -.-> role:debitor.debitorRel.contact:OWNER +role:debitor.debitorRel.contact:OWNER -.-> role:debitor.debitorRel.contact:ADMIN +role:debitor.debitorRel.contact:ADMIN -.-> role:debitor.debitorRel.contact:REFERRER +role:global:ADMIN -.-> role:debitor.debitorRel:OWNER +role:debitor.debitorRel:OWNER -.-> role:debitor.debitorRel:ADMIN +role:debitor.debitorRel:ADMIN -.-> role:debitor.debitorRel:AGENT +role:debitor.debitorRel:AGENT -.-> role:debitor.debitorRel:TENANT +role:debitor.debitorRel.contact:ADMIN -.-> role:debitor.debitorRel:TENANT +role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.anchorPerson:REFERRER +role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.holderPerson:REFERRER +role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.contact:REFERRER +role:debitor.debitorRel.anchorPerson:ADMIN -.-> role:debitor.debitorRel:OWNER +role:debitor.debitorRel.holderPerson:ADMIN -.-> role:debitor.debitorRel:AGENT +role:global:ADMIN -.-> role:debitor.refundBankAccount:OWNER +role:debitor.refundBankAccount:OWNER -.-> role:debitor.refundBankAccount:ADMIN +role:debitor.refundBankAccount:ADMIN -.-> role:debitor.refundBankAccount:REFERRER +role:debitor.refundBankAccount:ADMIN -.-> role:debitor.debitorRel:AGENT +role:debitor.debitorRel:AGENT -.-> role:debitor.refundBankAccount:REFERRER +role:global:ADMIN -.-> role:debitor.partnerRel.anchorPerson:OWNER +role:debitor.partnerRel.anchorPerson:OWNER -.-> role:debitor.partnerRel.anchorPerson:ADMIN +role:debitor.partnerRel.anchorPerson:ADMIN -.-> role:debitor.partnerRel.anchorPerson:REFERRER +role:global:ADMIN -.-> role:debitor.partnerRel.holderPerson:OWNER +role:debitor.partnerRel.holderPerson:OWNER -.-> role:debitor.partnerRel.holderPerson:ADMIN +role:debitor.partnerRel.holderPerson:ADMIN -.-> role:debitor.partnerRel.holderPerson:REFERRER +role:global:ADMIN -.-> role:debitor.partnerRel.contact:OWNER +role:debitor.partnerRel.contact:OWNER -.-> role:debitor.partnerRel.contact:ADMIN +role:debitor.partnerRel.contact:ADMIN -.-> role:debitor.partnerRel.contact:REFERRER +role:global:ADMIN -.-> role:debitor.partnerRel:OWNER +role:debitor.partnerRel:OWNER -.-> role:debitor.partnerRel:ADMIN +role:debitor.partnerRel:ADMIN -.-> role:debitor.partnerRel:AGENT +role:debitor.partnerRel:AGENT -.-> role:debitor.partnerRel:TENANT +role:debitor.partnerRel.contact:ADMIN -.-> role:debitor.partnerRel:TENANT +role:debitor.partnerRel:TENANT -.-> role:debitor.partnerRel.anchorPerson:REFERRER +role:debitor.partnerRel:TENANT -.-> role:debitor.partnerRel.holderPerson:REFERRER +role:debitor.partnerRel:TENANT -.-> role:debitor.partnerRel.contact:REFERRER +role:debitor.partnerRel.anchorPerson:ADMIN -.-> role:debitor.partnerRel:OWNER +role:debitor.partnerRel.holderPerson:ADMIN -.-> role:debitor.partnerRel:AGENT +role:debitor.partnerRel:ADMIN -.-> role:debitor.debitorRel:ADMIN +role:debitor.partnerRel:AGENT -.-> role:debitor.debitorRel:AGENT +role:debitor.debitorRel:AGENT -.-> role:debitor.partnerRel:TENANT +role:global:ADMIN -.-> role:debitorRel.anchorPerson:OWNER +role:debitorRel.anchorPerson:OWNER -.-> role:debitorRel.anchorPerson:ADMIN +role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel.anchorPerson:REFERRER +role:global:ADMIN -.-> role:debitorRel.holderPerson:OWNER +role:debitorRel.holderPerson:OWNER -.-> role:debitorRel.holderPerson:ADMIN +role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER +role:global:ADMIN -.-> role:debitorRel.contact:OWNER +role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN +role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER +role:global:ADMIN -.-> role:debitorRel:OWNER +role:debitorRel:OWNER -.-> role:debitorRel:ADMIN +role:debitorRel:ADMIN -.-> role:debitorRel:AGENT +role:debitorRel:AGENT -.-> role:debitorRel:TENANT +role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT +role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER +role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER +role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER +role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER +role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT +role:debitorRel:AGENT ==> role:bookingItem:OWNER +role:bookingItem:OWNER ==> role:bookingItem:ADMIN +role:bookingItem:ADMIN ==> role:bookingItem:TENANT +role:bookingItem:TENANT ==> role:debitorRel:TENANT + +%% granting permissions to roles +role:debitorRel:ADMIN ==> perm:bookingItem:INSERT +role:global:ADMIN ==> perm:bookingItem:DELETE +role:bookingItem:OWNER ==> perm:bookingItem:UPDATE +role:bookingItem:TENANT ==> perm:bookingItem:SELECT + +``` diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql new file mode 100644 index 00000000..8dd6b954 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql @@ -0,0 +1,194 @@ +--liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator, do not amend manually. + + +-- ============================================================================ +--changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRelatedRbacObject('hs_booking_item'); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item'); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + */ + +create or replace procedure buildRbacSystemForHsBookingItem( + NEW hs_booking_item +) + language plpgsql as $$ + +declare + newDebitor hs_office_debitor; + newDebitorRel hs_office_relation; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM hs_office_debitor WHERE uuid = NEW.debitorUuid INTO newDebitor; + assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + + SELECT debitorRel.* + FROM hs_office_relation debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = NEW.debitorUuid + INTO newDebitorRel; + assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + + + perform createRoleWithGrants( + hsBookingItemOWNER(NEW), + permissions => array['UPDATE'], + incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)] + ); + + perform createRoleWithGrants( + hsBookingItemADMIN(NEW), + incomingSuperRoles => array[hsBookingItemOWNER(NEW)] + ); + + perform createRoleWithGrants( + hsBookingItemTENANT(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[hsBookingItemADMIN(NEW)], + outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)] + ); + + call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), globalAdmin()); + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row. + */ + +create or replace function insertTriggerForHsBookingItem_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForHsBookingItem(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForHsBookingItem_tg + after insert on hs_booking_item + for each row +execute procedure insertTriggerForHsBookingItem_tf(); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates INSERT INTO hs_booking_item permissions for the related hs_office_relation rows. + */ +do language plpgsql $$ + declare + row hs_office_relation; + begin + call defineContext('create INSERT INTO hs_booking_item permissions for the related hs_office_relation rows'); + + FOR row IN SELECT * FROM hs_office_relation + LOOP + call grantPermissionToRole( + createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + hsOfficeRelationADMIN(row)); + END LOOP; + END; +$$; + +/** + Adds hs_booking_item INSERT permission to specified role of new hs_office_relation rows. +*/ +create or replace function hs_booking_item_hs_office_relation_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), + hsOfficeRelationADMIN(NEW)); + return NEW; +end; $$; + +-- z_... is to put it at the end of after insert triggers, to make sure the roles exist +create trigger z_hs_booking_item_hs_office_relation_insert_tg + after insert on hs_office_relation + for each row +execute procedure hs_booking_item_hs_office_relation_insert_tf(); + +/** + Checks if the user or assumed roles are allowed to insert a row to hs_booking_item, + where the check is performed by an indirect role. + + An indirect role is a role which depends on an object uuid which is not a direct foreign key + of the source entity, but needs to be fetched via joined tables. +*/ +create or replace function hs_booking_item_insert_permission_check_tf() + returns trigger + language plpgsql as $$ + +declare + superRoleObjectUuid uuid; + +begin + superRoleObjectUuid := (SELECT debitorRel.uuid + FROM hs_office_relation debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = NEW.debitorUuid + ); + assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null'; + + if ( not hasInsertPermission(superRoleObjectUuid, 'INSERT', 'hs_booking_item') ) then + raise exception + '[403] insert into hs_booking_item not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end if; + return NEW; +end; $$; + +create trigger hs_booking_item_insert_permission_check_tg + before insert on hs_booking_item + for each row + execute procedure hs_booking_item_insert_permission_check_tf(); +--// + +-- ============================================================================ +--changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call generateRbacIdentityViewFromProjection('hs_booking_item', + $idName$ + caption + $idName$); +--// + +-- ============================================================================ +--changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRestrictedView('hs_booking_item', + $orderBy$ + caption + $orderBy$, + $updates$ + version = new.version, + validity = new.validity, + resources = new.resources + $updates$); +--// + diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql new file mode 100644 index 00000000..6102c2b7 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql @@ -0,0 +1,51 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset hs-booking-item-TEST-DATA-GENERATOR:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a single hs_booking_item test record. + */ +create or replace procedure createHsBookingItemTransactionTestData( + givenPartnerNumber numeric, + givenDebitorSuffix char(2), + givenCaption varchar + ) + language plpgsql as $$ +declare + currentTask varchar; + relatedDebitor hs_office_debitor; +begin + currentTask := 'creating booking-item test-data ' || givenPartnerNumber::text || givenDebitorSuffix; + call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); + execute format('set local hsadminng.currentTask to %L', currentTask); + + select debitor.* into relatedDebitor + from hs_office_debitor debitor + join hs_office_relation debitorRel on debitorRel.uuid = debitor.debitorRelUuid + join hs_office_relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid + join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid + where partner.partnerNumber = givenPartnerNumber and debitor.debitorNumberSuffix = givenDebitorSuffix; + + raise notice 'creating test booking-item: %', givenPartnerNumber::text || givenDebitorSuffix::text; + raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; + insert + into hs_booking_item (uuid, debitoruuid, caption, validity, resources) + values (uuid_generate_v4(), relatedDebitor.uuid, givenCaption, daterange('20221001' , null, '[]'), '{ "CPUs": 2, "HDD-storage": 512 }'::jsonb); +end; $$; +--// + + +-- ============================================================================ +--changeset hs-booking-item-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +-- ---------------------------------------------------------------------------- + +do language plpgsql $$ + begin + call createHsBookingItemTransactionTestData(10001, '11', 'some booking 1'); + call createHsBookingItemTransactionTestData(10002, '12', 'some booking 2'); + call createHsBookingItemTransactionTestData(10003, '13', 'some booking 3'); + end; +$$; diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 11a5f956..fccafed2 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -127,3 +127,9 @@ databaseChangeLog: file: db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql - include: file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql + - include: + file: db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql + - include: + file: db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql + - include: + file: db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java new file mode 100644 index 00000000..4376626b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -0,0 +1,396 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import io.hypersistence.utils.hibernate.type.range.Range; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.test.Accepts; +import net.hostsharing.test.JpaAttempt; +import org.json.JSONException; +import org.junit.jupiter.api.Nested; +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.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.util.UUID; + +import static net.hostsharing.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.startsWith; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +@Transactional +class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { + + @LocalServerPort + private Integer port; + + @Autowired + HsBookingItemRepository bookingItemRepo; + + @Autowired + HsOfficeDebitorRepository debitorRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @PersistenceContext + EntityManager em; + + @Nested + class ListBookingItems { + + @Test + void globalAdmin_canViewAllBookingItemsOfArbitraryDebitor() throws JSONException { + + // given + context("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).get(0); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/booking/items?debitorUuid" + givenDebitor.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .log().all() + .body("", lenientlyEquals(""" + [ + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + }, + { + "debitor": { "debitorNumber": 1000212 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + }, + { + "debitor": { "debitorNumber": 1000313 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + ] + """)); + // @formatter:on + } + } + + @Nested + class AddBookingItem { + + @Test + void globalAdmin_canAddBookingItem() { + + context.define("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "debitorUuid": "%s", + "validFrom": "2022-10-13" + } + """.formatted(givenDebitor.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/office/BookingItems") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + """)) + .header("Location", startsWith("http://localhost")) + .extract().header("Location"); // @formatter:on + + // finally, the new bookingItem can be accessed under the generated UUID + final var newUserUuid = UUID.fromString( + location.substring(location.lastIndexOf('/') + 1)); + assertThat(newUserUuid).isNotNull(); + } + } + + @Nested + class GetBookingItem { + + @Test + void globalAdmin_canGetArbitraryBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItemUuid = bookingItemRepo.findAll().stream() + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) + .findAny().orElseThrow().getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + """)); // @formatter:on + } + + @Test + void normalUser_canNotGetUnrelatedBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItemUuid = bookingItemRepo.findAll().stream() + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) + .findAny().orElseThrow().getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + } + + @Test + void debitorAgentUser_canGetRelatedBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItemUuid = bookingItemRepo.findAll().stream() + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) + .findAny().orElseThrow().getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "person-FirbySusan@example.com") + .port(port) + .when() + .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) + .then().log().body().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + """)); // @formatter:on + } + } + + @Nested + class PatchBookingItem { + + @Test + void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() { + + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "validFrom": "2020-06-05", + "validTo": "2022-12-31", + "resources": { + "CPUs": "2", + "SSD-storage": "512" + } + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + """)); // @formatter:on + + // finally, the bookingItem is actually updated + context.define("superuser-alex@hostsharing.net"); + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() + .matches(mandate -> { + assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); + assertThat(mandate.getValidFrom()).isEqualTo("2020-06-05"); + assertThat(mandate.getValidTo()).isEqualTo("2022-12-31"); + return true; + }); + } + + @Test + void globalAdmin_canPatchJustValidToOfArbitraryBookingItem() { + + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "validTo": "2022-12-31" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "debitor": { "debitorNumber": 1000111 }, + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + } + """)); // @formatter:on + + // finally, the bookingItem is actually updated + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() + .matches(mandate -> { + assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); + assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)"); + return true; + }); + } + + @Test + void globalAdmin_canNotPatchReferenceOfArbitraryBookingItem() { + + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "reference": "temp ref CAT new" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().assertThat() + // TODO.impl: I'd prefer a 400, + // but OpenApi Spring Code Gen does not convert additonalProperties=false into a validation + .statusCode(200); // @formatter:on + + // finally, the bookingItem is actually updated + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() + .matches(mandate -> { + assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-03-31)"); + return true; + }); + } + } + + @Nested + class DeleteBookingItem { + + @Test + void globalAdmin_canDeleteArbitraryBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given bookingItem is gone + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isEmpty(); + } + + @Test + void bankAccountAdminUser_canNotDeleteRelatedBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + RestAssured // @formatter:off + .given() + .header("current-user", "bankaccount-admin@FirstGmbH.example.com") + .port(port) + .when() + .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().log().body().assertThat() + .statusCode(403); // @formatter:on + + // then the given bookingItem is still there + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isNotEmpty(); + } + + @Test + @Accepts({ "BookingItem:X(Access Control)" }) + void normalUser_canNotDeleteUnrelatedBookingItem() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + + // then the given bookingItem is still there + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isNotEmpty(); + } + } + + private HsBookingItemEntity givenSomeTemporaryBookingItemForDebitorNumber(final int debitorNumber) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var newBookingItem = HsBookingItemEntity.builder() + .uuid(UUID.randomUUID()) + .debitor(givenDebitor) + .validity(Range.closedOpen( + LocalDate.parse("2022-11-01"), LocalDate.parse("2023-03-31"))) + .build(); + + return bookingItemRepo.save(newBookingItem); + }).assertSuccessful().returnedValue(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java new file mode 100644 index 00000000..a90baf38 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -0,0 +1,104 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import io.hypersistence.utils.hibernate.type.range.Range; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.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.time.LocalDate; +import java.util.UUID; +import java.util.stream.Stream; + +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntityPatcher.objectToMap; +import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; + +@TestInstance(PER_CLASS) +@ExtendWith(MockitoExtension.class) +class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< + HsBookingItemPatchResource, + HsBookingItemEntity + > { + + private static final UUID INITIAL_BOOKING_ITEM_UUID = UUID.randomUUID(); + private static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-04-15"); + private static final LocalDate PATCHED_VALID_FROM = LocalDate.parse("2022-10-30"); + private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); + + private static final ArbitraryBookingResourcesJsonResource PATCHED_RESOURCES = new ArbitraryBookingResourcesJsonResource() { + int cpus = 2; + int sddStorage = 256; + int hddStorage = 2048; + }; + private static final String PATCHED_CAPTION = "caption-patched"; + + @Mock + private EntityManager em; + + @BeforeEach + void initMocks() { + lenient().when(em.getReference(eq(HsOfficeDebitorEntity.class), any())).thenAnswer(invocation -> + HsOfficeDebitorEntity.builder().uuid(invocation.getArgument(1)).build()); + lenient().when(em.getReference(eq(HsBookingItemEntity.class), any())).thenAnswer(invocation -> + HsBookingItemEntity.builder().uuid(invocation.getArgument(1)).build()); + } + + @Override + protected HsBookingItemEntity newInitialEntity() { + final var entity = new HsBookingItemEntity(); + entity.setUuid(INITIAL_BOOKING_ITEM_UUID); + entity.setDebitor(TEST_DEBITOR); + entity.setResources(objectToMap(PATCHED_RESOURCES)); + entity.setCaption(PATCHED_CAPTION); + entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); + return entity; + } + + @Override + protected HsBookingItemPatchResource newPatchResource() { + return new HsBookingItemPatchResource(); + } + + @Override + protected HsBookingItemEntityPatcher createPatcher(final HsBookingItemEntity sepaMandate) { + return new HsBookingItemEntityPatcher(sepaMandate); + } + + @Override + protected Stream propertyTestDescriptors() { + return Stream.of( + new JsonNullableProperty<>( + "caption", + HsBookingItemPatchResource::setCaption, + PATCHED_CAPTION, + HsBookingItemEntity::setCaption), +// FIXME +// new JsonNullableProperty<>( +// "resources", +// HsBookingItemPatchResource::setResources, +// PATCHED_RESOURCES, +// HsBookingItemEntity::setResources, +// objectToMap(PATCHED_RESOURCES)), + new JsonNullableProperty<>( + "validfrom", + HsBookingItemPatchResource::setValidFrom, + PATCHED_VALID_FROM, + HsBookingItemEntity::setValidFrom), + new JsonNullableProperty<>( + "validto", + HsBookingItemPatchResource::setValidTo, + PATCHED_VALID_TO, + HsBookingItemEntity::setValidTo) + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java new file mode 100644 index 00000000..c5513fa9 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java @@ -0,0 +1,56 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; +import static org.assertj.core.api.Assertions.assertThat; + +class HsBookingItemEntityUnitTest { + public static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-01-01"); + public static final LocalDate GIVEN_VALID_TO = LocalDate.parse("2030-12-31"); + + final HsBookingItemEntity givenBookingItem = HsBookingItemEntity.builder() + .debitor(TEST_DEBITOR) + .caption("some caption") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("SSD-storage", 512), + entry("HDD-storage", 2048))) + .validity(toPostgresDateRange(GIVEN_VALID_FROM, GIVEN_VALID_TO)) + .build(); + + @Test + void toStringContainsAllPropertiesAndResourcesSortedByKey() { + final var result = givenBookingItem.toString(); + + assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, some caption, [2020-01-01,2031-01-01), {CPUs=2, HDD-storage=2048, SSD-storage=512})"); + } + + @Test + void toShortStringContainsOnlyMemberNumberAndCaption() { + final var result = givenBookingItem.toShortString(); + + assertThat(result).isEqualTo("D-1000100:some caption"); + } + + @Test + void settingValidFromKeepsValidTo() { + givenBookingItem.setValidFrom(LocalDate.parse("2023-12-31")); + assertThat(givenBookingItem.getValidFrom()).isEqualTo(LocalDate.parse("2023-12-31")); + assertThat(givenBookingItem.getValidTo()).isEqualTo(GIVEN_VALID_TO); + + } + + @Test + void settingValidToKeepsValidFrom() { + givenBookingItem.setValidTo(LocalDate.parse("2024-12-31")); + assertThat(givenBookingItem.getValidFrom()).isEqualTo(GIVEN_VALID_FROM); + assertThat(givenBookingItem.getValidTo()).isEqualTo(LocalDate.parse("2024-12-31")); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java new file mode 100644 index 00000000..fbce87ae --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -0,0 +1,341 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import io.hypersistence.utils.hibernate.type.range.Range; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; +import net.hostsharing.test.Array; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.orm.jpa.JpaSystemException; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; +import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; +import static net.hostsharing.test.Array.fromFormatted; +import static net.hostsharing.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({ Context.class, JpaAttempt.class }) +class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + HsBookingItemRepository bookingItemRepo; + + @Autowired + HsOfficeDebitorRepository debitorRepo; + + @Autowired + RawRbacRoleRepository rawRoleRepo; + + @Autowired + RawRbacGrantRepository rawGrantRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @PersistenceContext + EntityManager em; + + @MockBean + HttpServletRequest request; + + @Nested + class CreateBookingItem { + + @Test + public void testHostsharingAdmin_withoutAssumedRole_canCreateNewBookingItem() { + // given + context("superuser-alex@hostsharing.net"); + final var count = bookingItemRepo.count(); + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + + // when + final var result = attempt(em, () -> { + final var newBookingItem = HsBookingItemEntity.builder() + .debitor(givenDebitor) + .caption("some new booking item") + .validity(Range.closedOpen( + LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) + .build(); + return toCleanup(bookingItemRepo.save(newBookingItem)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsBookingItemEntity::getUuid).isNotNull(); + assertThatBookingItemIsPersisted(result.returnedValue()); + assertThat(bookingItemRepo.count()).isEqualTo(count + 1); + } + + @Test + public void createsAndGrantsRoles() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() + .map(s -> s.replace("hs_office_", "")) + .toList(); + + // when + attempt(em, () -> { + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var newBookingItem = HsBookingItemEntity.builder() + .debitor(givenDebitor) + .caption("some new booking item") + .validity(Range.closedOpen( + LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) + .build(); + return toCleanup(bookingItemRepo.save(newBookingItem)); + }); + + // then + final var all = rawRoleRepo.findAll(); + assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( + initialRoleNames, + "hs_booking_item#somenewbookingitem:ADMIN", + "hs_booking_item#somenewbookingitem:OWNER", + "hs_booking_item#somenewbookingitem:TENANT")); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) + .map(s -> s.replace("hs_office_", "")) + .containsExactlyInAnyOrder(fromFormatted( + initialGrantNames, + + // insert+delete + "{ grant perm:hs_booking_item#somenewbookingitem:DELETE to role:global#global:ADMIN by system and assume }", + + // owner + + // admin + + // tenant + + "{ grant perm:hs_booking_item#somenewbookingitem:SELECT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:UPDATE to role:hs_booking_item#somenewbookingitem:OWNER by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:ADMIN to role:hs_booking_item#somenewbookingitem:OWNER by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:TENANT to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", + null)); + } + + private void assertThatBookingItemIsPersisted(final HsBookingItemEntity saved) { + final var found = bookingItemRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().map(HsBookingItemEntity::toString).get().isEqualTo(saved.toString()); + } + } + + @Nested + class FindByDebitorUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllBookingItemsOfArbitraryDebitor() { + // given + context("superuser-alex@hostsharing.net"); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); + + // when + final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + + // then + allTheseBookingItemsAreReturned( + result, + "HsBookingItemEntity(D-1000111, some booking 1, [2022-10-01,), {CPUs=2, HDD-storage=512})"); + } + + @Test + public void normalUser_canViewOnlyRelatedBookingItems() { + // given: + context("bankaccount-admin@FirstGmbH.example.com"); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); + + // when: + final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + + // then: + exactlyTheseBookingItemsAreReturned( + result, + "HsBookingItemEntity(D-1000111, some booking 1, [2022-10-01,), {CPUs=2, HDD-storage=512})"); + } + } + + @Nested + class UpdateBookingItem { + + @Test + public void hostsharingAdmin_canUpdateArbitraryBookingItem() { + // given + final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + givenBookingItem.setResources(Map.ofEntries( + entry("CPUs", 2), + entry("SSD-storage", 512), + entry("HDD-storage", 2048))); + givenBookingItem.setValidity(Range.closedOpen( + LocalDate.parse("2019-05-17"), LocalDate.parse("2023-01-01"))); + return toCleanup(bookingItemRepo.save(givenBookingItem)); + }); + + // then + result.assertSuccessful(); + jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + assertThatBookingItemActuallyInDatabase(result.returnedValue()); + }).assertSuccessful(); + } + + private void assertThatBookingItemActuallyInDatabase(final HsBookingItemEntity saved) { + final var found = bookingItemRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().isNotSameAs(saved) + .extracting(Object::toString).isEqualTo(saved.toString()); + } + + private void assertThatBookingItemIsVisibleForUserWithRole( + final HsBookingItemEntity entity, + final String assumedRoles) { + jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", assumedRoles); + assertThatBookingItemActuallyInDatabase(entity); + }).assertSuccessful(); + } + } + + @Nested + class DeleteByUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canDeleteAnyBookingItem() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + bookingItemRepo.deleteByUuid(givenBookingItem.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-fran@hostsharing.net", null); + return bookingItemRepo.findByUuid(givenBookingItem.getUuid()); + }).assertSuccessful().returnedValue()).isEmpty(); + } + + @Test + public void nonGlobalAdmin_canNotDeleteTheirRelatedBookingItem() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("person-FirbySusan@example.com"); + assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent(); + + bookingItemRepo.deleteByUuid(givenBookingItem.getUuid()); + }); + + // then + result.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "[403] Subject ", " is not allowed to delete hs_booking_item"); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return bookingItemRepo.findByUuid(givenBookingItem.getUuid()); + }).assertSuccessful().returnedValue()).isPresent(); // still there + } + + @Test + public void deletingABookingItemAlsoDeletesRelatedRolesAndGrants() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); + final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); + final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return bookingItemRepo.deleteByUuid(givenBookingItem.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isEqualTo(1); + assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); + } + } + + @Test + public void auditJournalLogIsAvailable() { + // given + final var query = em.createNativeQuery(""" + select currentTask, targetTable, targetOp + from tx_journal_v + where targettable = 'hs_booking_item'; + """); + + // when + @SuppressWarnings("unchecked") final List customerLogEntries = query.getResultList(); + + // then + assertThat(customerLogEntries).map(Arrays::toString).contains( + "[creating booking-item test-data 1000111, hs_booking_item, INSERT]", + "[creating booking-item test-data 1000111, hs_booking_item, INSERT]", + "[creating booking-item test-data 1000111, hs_booking_item, INSERT]"); + } + + private HsBookingItemEntity givenSomeTemporaryBookingItem(final int debitorNumber) { + return jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var newBookingItem = HsBookingItemEntity.builder() + .debitor(givenDebitor) + .caption("some temp booking item") + .validity(Range.closedOpen( + LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) + .resources(Map.ofEntries( + entry("CPUs", 1), + entry("SSD-storage", 256))) + .build(); + + return toCleanup(bookingItemRepo.save(newBookingItem)); + }).assertSuccessful().returnedValue(); + } + + void exactlyTheseBookingItemsAreReturned( + final List actualResult, + final String... bookingItemNames) { + assertThat(actualResult) + .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .containsExactlyInAnyOrder(bookingItemNames); + } + + void allTheseBookingItemsAreReturned(final List actualResult, final String... bookingItemNames) { + assertThat(actualResult) + .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .contains(bookingItemNames); + } +} -- 2.39.5 From 75c759bed37b970759f2b876fd63eb064d2df4f1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 10:30:08 +0200 Subject: [PATCH 02/21] fix acceptance tests --- .../booking/item/HsBookingItemController.java | 19 ++- .../hs/booking/item/HsBookingItemEntity.java | 1 + .../HsOfficeBankAccountController.java | 4 +- .../hs-booking/api-mappings.yaml | 2 +- .../hs-booking/hs-booking-item-schemas.yaml | 3 - .../hs-booking-items-with-uuid.yaml | 6 +- .../hs-booking/hs-booking-items.yaml | 6 +- .../api-definition/hs-booking/hs-booking.yaml | 2 +- .../6018-hs-booking-item-test-data.sql | 13 +- .../hsadminng/arch/ArchitectureTest.java | 14 +- ...HsBookingItemControllerAcceptanceTest.java | 124 ++++++++++-------- ...sBookingItemRepositoryIntegrationTest.java | 10 +- ...fficeDebitorRepositoryIntegrationTest.java | 1 + 13 files changed, 118 insertions(+), 87 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 4aeb8172..b74e7e0c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -1,11 +1,11 @@ package net.hostsharing.hsadminng.hs.booking.item; +import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; import net.hostsharing.hsadminng.mapper.Mapper; -import net.hostsharing.hsadminng.context.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -76,10 +76,10 @@ public class HsBookingItemController implements HsBookingItemsApi { context.define(currentUser, assumedRoles); final var result = bookingItemRepo.findByUuid(bookingItemUuid); - if (result.isEmpty()) { - return ResponseEntity.notFound().build(); - } - return ResponseEntity.ok(mapper.map(result.get(), HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)); + return result + .map(bookingItemEntity -> ResponseEntity.ok( + mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) + .orElseGet(() -> ResponseEntity.notFound().build()); } @Override @@ -91,11 +91,9 @@ public class HsBookingItemController implements HsBookingItemsApi { context.define(currentUser, assumedRoles); final var result = bookingItemRepo.deleteByUuid(bookingItemUuid); - if (result == 0) { - return ResponseEntity.notFound().build(); - } - - return ResponseEntity.noContent().build(); + return result == 0 + ? ResponseEntity.notFound().build() + : ResponseEntity.noContent().build(); } @Override @@ -122,7 +120,6 @@ public class HsBookingItemController implements HsBookingItemsApi { if (entity.getValidity().hasUpperBound()) { resource.setValidTo(entity.getValidity().upper().minusDays(1)); } - resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber()); }; final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index c5edf1b1..cf29c709 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -118,6 +118,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { public static RbacView rbac() { return rbacViewFor("bookingItem", HsBookingItemEntity.class) .withIdentityView(SQL.projection("caption")) // FIXME: use memberNumber:caption + .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "validity", "resources") .importEntityAlias("debitor", HsOfficeDebitorEntity.class, diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java index 764d0a4a..9f39767f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountController.java @@ -74,11 +74,11 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi { public ResponseEntity getBankAccountByUuid( final String currentUser, final String assumedRoles, - final UUID BankAccountUuid) { + final UUID bankAccountUuid) { context.define(currentUser, assumedRoles); - final var result = bankAccountRepo.findByUuid(BankAccountUuid); + final var result = bankAccountRepo.findByUuid(bankAccountUuid); if (result.isEmpty()) { return ResponseEntity.notFound().build(); } diff --git a/src/main/resources/api-definition/hs-booking/api-mappings.yaml b/src/main/resources/api-definition/hs-booking/api-mappings.yaml index 9faac586..bce81426 100644 --- a/src/main/resources/api-definition/hs-booking/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-booking/api-mappings.yaml @@ -14,5 +14,5 @@ map: - type: string:format => java.lang.String paths: - /api/hs/booking/items/{itemUUID}: + /api/hs/booking/items/{bookingItemUuid}: null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 1b0fb77c..e06a5ffd 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -9,8 +9,6 @@ components: uuid: type: string format: uuid - debitor: - $ref: '../hs-office/hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor' caption: type: string validFrom: @@ -44,7 +42,6 @@ components: nullable: true resources: $ref: '#/components/schemas/ArbitraryBookingResourcesJson' - additionalProperties: false HsBookingItemInsert: type: object diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml index 955e45b8..99a8803a 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml @@ -6,7 +6,7 @@ get: parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - - name: bookingItemUUID + - name: bookingItemUuid in: path required: true schema: @@ -34,7 +34,7 @@ patch: parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - - name: bookingItemUUID + - name: bookingItemUuid in: path required: true schema: @@ -65,7 +65,7 @@ delete: parameters: - $ref: './auth.yaml#/components/parameters/currentUser' - $ref: './auth.yaml#/components/parameters/assumedRoles' - - name: bookingItemUUID + - name: bookingItemUuid in: path required: true schema: diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml index 4fd8b650..1e6679b4 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml @@ -1,6 +1,6 @@ get: - summary: Returns a list of (optionally filtered) booking items. - description: Returns the list of (optionally filtered) booking items which are visible to the current user or any of it's assumed roles. + summary: Returns a list of all booking items for a specified debitor. + description: Returns the list of all booking items for a specified debitor which are visible to the current user or any of it's assumed roles. tags: - hs-booking-items operationId: listBookingItemsByDebitorUuid @@ -9,7 +9,7 @@ get: - $ref: './auth.yaml#/components/parameters/assumedRoles' - name: debitorUuid in: query - required: false + required: true schema: type: string format: uuid diff --git a/src/main/resources/api-definition/hs-booking/hs-booking.yaml b/src/main/resources/api-definition/hs-booking/hs-booking.yaml index e2311b85..8da21d09 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking.yaml @@ -13,5 +13,5 @@ paths: /api/hs/booking/items: $ref: "./hs-booking-items.yaml" - /api/hs/booking/items/{itemUUID}: + /api/hs/booking/items/{bookingItemUuid}: $ref: "./hs-booking-items-with-uuid.yaml" diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql index 6102c2b7..7a688e42 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql @@ -10,8 +10,7 @@ */ create or replace procedure createHsBookingItemTransactionTestData( givenPartnerNumber numeric, - givenDebitorSuffix char(2), - givenCaption varchar + givenDebitorSuffix char(2) ) language plpgsql as $$ declare @@ -33,7 +32,9 @@ begin raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; insert into hs_booking_item (uuid, debitoruuid, caption, validity, resources) - values (uuid_generate_v4(), relatedDebitor.uuid, givenCaption, daterange('20221001' , null, '[]'), '{ "CPUs": 2, "HDD-storage": 512 }'::jsonb); + values (uuid_generate_v4(), relatedDebitor.uuid, 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "SDD-storage": 512 }'::jsonb), + (uuid_generate_v4(), relatedDebitor.uuid, 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "HDD-storage": 1024 }'::jsonb), + (uuid_generate_v4(), relatedDebitor.uuid, 'some Whatever', daterange('20240401', null, '[]'), '{ "CPUs": 1, "SDD-storage": 512, "HDD-storage": 2048 }'::jsonb); end; $$; --// @@ -44,8 +45,8 @@ end; $$; do language plpgsql $$ begin - call createHsBookingItemTransactionTestData(10001, '11', 'some booking 1'); - call createHsBookingItemTransactionTestData(10002, '12', 'some booking 2'); - call createHsBookingItemTransactionTestData(10003, '13', 'some booking 3'); + call createHsBookingItemTransactionTestData(10001, '11'); + call createHsBookingItemTransactionTestData(10002, '12'); + call createHsBookingItemTransactionTestData(10003, '13'); end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index a1ee752a..1523f7cf 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -50,6 +50,7 @@ public class ArchitectureTest { "..hs.office.person", "..hs.office.relation", "..hs.office.sepamandate", + "..hs.booking.item", "..errors", "..mapper", "..ping", @@ -123,11 +124,22 @@ public class ArchitectureTest { @ArchTest @SuppressWarnings("unused") - public static final ArchRule hsAdminPackagesRule = classes() + public static final ArchRule hsOfficePackageAccessRule = classes() .that().resideInAPackage("..hs.office.(*)..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage( "..hs.office.(*)..", + "..hs.booking.(*)..", + "..rbac.rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest + ); + + @ArchTest + @SuppressWarnings("unused") + public static final ArchRule hsBookingPackageAccessRule = classes() + .that().resideInAPackage("..hs.booking.(*)..") + .should().onlyBeAccessed().byClassesThat() + .resideInAnyPackage( + "..hs.booking.(*)..", "..rbac.rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 4376626b..a21fd863 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -19,8 +19,10 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.time.LocalDate; +import java.util.Map; import java.util.UUID; +import static java.util.Map.entry; import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.startsWith; @@ -62,28 +64,31 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/booking/items?debitorUuid" + givenDebitor.getUuid()) + .get("http://localhost/api/hs/booking/items?debitorUuid=" + givenDebitor.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .log().all() .body("", lenientlyEquals(""" [ - { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - }, - { - "debitor": { "debitorNumber": 1000212 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - }, - { - "debitor": { "debitorNumber": 1000313 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - } + { + "caption": "some ManagedServer", + "validFrom": "2022-10-01", + "validTo": null, + "resources": null + }, + { + "caption": "some CloudServer", + "validFrom": "2023-01-15", + "validTo": "2024-04-14", + "resources": null + }, + { + "caption": "some Whatever", + "validFrom": "2024-04-01", + "validTo": null, + "resources": null + } ] """)); // @formatter:on @@ -97,30 +102,35 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).get(0); final var location = RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" - { - "debitorUuid": "%s", - "validFrom": "2022-10-13" - } + { + "debitorUuid": "%s", + "caption": "some new booking", + "resources": { + "something": 12 + }, + "validFrom": "2022-10-13" + } """.formatted(givenDebitor.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/office/BookingItems") + .post("http://localhost/api/hs/booking/items") .then().log().all().assertThat() .statusCode(201) .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - } + "caption": "some new booking", + "validFrom": "2022-10-13", + "validTo": null, + "resources": null + } """)) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on @@ -139,7 +149,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000111) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -148,15 +158,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .port(port) .when() .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) - .then().log().body().assertThat() + .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - } + "caption": "some CloudServer", + "validFrom": "2023-01-15", + "validTo": "2024-04-14", + "resources": null + } """)); // @formatter:on } @@ -164,8 +175,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void normalUser_canNotGetUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) - .findAny().orElseThrow().getUuid(); + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000212) + .map(HsBookingItemEntity::getUuid) + .findAny().orElseThrow(); RestAssured // @formatter:off .given() @@ -181,23 +193,25 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void debitorAgentUser_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000101) + .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000313) .findAny().orElseThrow().getUuid(); + generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "booking-item-of-debitor-1000313"); RestAssured // @formatter:off .given() - .header("current-user", "person-FirbySusan@example.com") + .header("current-user", "person-TuckerJack@example.com") .port(port) .when() .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) - .then().log().body().assertThat() + .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" + "caption": "some CloudServer", + "validFrom": "2023-01-15", + "validTo": "2024-04-14", + "resources": null } """)); // @formatter:on } @@ -227,16 +241,17 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup """) .port(port) .when() - .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .patch("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - } + "caption": "some test-booking", + "validFrom": "2020-06-05", + "validTo": "2022-12-31", + "resources": null + } """)); // @formatter:on // finally, the bookingItem is actually updated @@ -267,15 +282,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup """) .port(port) .when() - .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .patch("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "debitor": { "debitorNumber": 1000111 }, - "validFrom": "2022-10-01", - "validTo": "2026-12-31" + "caption": "some test-booking", + "validFrom": "2022-11-01", + "validTo": "2022-12-31", + "resources": null } """)); // @formatter:on @@ -305,7 +321,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup """) .port(port) .when() - .patch("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .patch("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().assertThat() // TODO.impl: I'd prefer a 400, // but OpenApi Spring Code Gen does not convert additonalProperties=false into a validation @@ -333,7 +349,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .delete("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().body().assertThat() .statusCode(204); // @formatter:on @@ -351,7 +367,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-user", "bankaccount-admin@FirstGmbH.example.com") .port(port) .when() - .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .delete("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().body().assertThat() .statusCode(403); // @formatter:on @@ -370,7 +386,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .header("current-user", "selfregistered-user-drew@hostsharing.org") .port(port) .when() - .delete("http://localhost/api/hs/office/BookingItems/" + givenBookingItem.getUuid()) + .delete("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().body().assertThat() .statusCode(404); // @formatter:on @@ -386,6 +402,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup final var newBookingItem = HsBookingItemEntity.builder() .uuid(UUID.randomUUID()) .debitor(givenDebitor) + .caption("some test-booking") + .resources(Map.ofEntries(entry("something", 1))) .validity(Range.closedOpen( LocalDate.parse("2022-11-01"), LocalDate.parse("2023-03-31"))) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index fbce87ae..171535a4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -148,7 +148,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup public void globalAdmin_withoutAssumedRole_canViewAllBookingItemsOfArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream().findAny().orElseThrow().getUuid(); // when final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); @@ -156,7 +156,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, some booking 1, [2022-10-01,), {CPUs=2, HDD-storage=512})"); + "HsBookingItemEntity(D-1000212, some CloudServer, [2023-01-15,2024-04-15), {CPUs=2, HDD-storage=1024})", + "HsBookingItemEntity(D-1000212, some ManagedServer, [2022-10-01,), {CPUs=2, SDD-storage=512})", + "HsBookingItemEntity(D-1000212, some Whatever, [2024-04-01,), {CPUs=1, HDD-storage=2048, SDD-storage=512})"); } @Test @@ -171,7 +173,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, some booking 1, [2022-10-01,), {CPUs=2, HDD-storage=512})"); + "HsBookingItemEntity(D-1000111, some CloudServer, [2023-01-15,2024-04-15), {CPUs=2, HDD-storage=1024})", + "HsBookingItemEntity(D-1000111, some ManagedServer, [2022-10-01,), {CPUs=2, SDD-storage=512})", + "HsBookingItemEntity(D-1000111, some Whatever, [2024-04-01,), {CPUs=1, HDD-storage=2048, SDD-storage=512})"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index 5224d6e9..8adaa224 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -181,6 +181,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>sepamandate to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>hs_booking_item to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", // owner "{ grant perm:debitor#D-1000122:DELETE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER by system and assume }", -- 2.39.5 From e46813bf9cdeed035ff2abbeb4be27838183344b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 11:13:57 +0200 Subject: [PATCH 03/21] cleanup --- ...HsBookingItemControllerAcceptanceTest.java | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index a21fd863..25d4478f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -6,9 +6,7 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; -import net.hostsharing.test.Accepts; import net.hostsharing.test.JpaAttempt; -import org.json.JSONException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -53,7 +51,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup class ListBookingItems { @Test - void globalAdmin_canViewAllBookingItemsOfArbitraryDebitor() throws JSONException { + void globalAdmin_canViewAllBookingItemsOfArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); @@ -358,25 +356,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void bankAccountAdminUser_canNotDeleteRelatedBookingItem() { - context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); - - RestAssured // @formatter:off - .given() - .header("current-user", "bankaccount-admin@FirstGmbH.example.com") - .port(port) - .when() - .delete("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) - .then().log().body().assertThat() - .statusCode(403); // @formatter:on - - // then the given bookingItem is still there - assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isNotEmpty(); - } - - @Test - @Accepts({ "BookingItem:X(Access Control)" }) void normalUser_canNotDeleteUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); -- 2.39.5 From 38b1c20f64b8bb734af4d77a5d0dc70973443a34 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 13:27:24 +0200 Subject: [PATCH 04/21] patch generated rbac trigger to fix too broad INSERT grants --- .../601-booking-item/6013-hs-booking-item-rbac.sql | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql index 8dd6b954..aa138789 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql @@ -104,25 +104,28 @@ do language plpgsql $$ call defineContext('create INSERT INTO hs_booking_item permissions for the related hs_office_relation rows'); FOR row IN SELECT * FROM hs_office_relation + WHERE type in ('DEBITOR') -- TODO.rbac: currently manually patched, needs to be generated LOOP call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', 'hs_booking_item'), - hsOfficeRelationADMIN(row)); + createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + hsOfficeRelationADMIN(row)); END LOOP; END; $$; /** - Adds hs_booking_item INSERT permission to specified role of new hs_office_relation rows. + Adds hs_booking_item INSERT permission to specified roleNSERT permission to specified role of new hs_office_relation rows. */ create or replace function hs_booking_item_hs_office_relation_insert_tf() returns trigger language plpgsql strict as $$ begin - call grantPermissionToRole( + if NEW.type = 'DEBITOR' then -- TODO.rbac: currently manually patched, needs to be generated + call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), hsOfficeRelationADMIN(NEW)); + end if; return NEW; end; $$; -- 2.39.5 From c7593a1e4fcead8a9d8d7cfd714fb6eecca18a6b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 13:40:58 +0200 Subject: [PATCH 05/21] fix ImportOfficeData --- .../hsadminng/hs/office/migration/ImportOfficeData.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 74155942..a158ae9f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -613,6 +613,7 @@ public class ImportOfficeData extends ContextBasedTest { private void deleteTestDataFromHsOfficeTables() { jpaAttempt.transacted(() -> { context(rbacSuperuser); + em.createNativeQuery("delete from hs_booking_item where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopassetstransaction where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopassetstransaction_legacy_id where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopsharestransaction where true").executeUpdate(); -- 2.39.5 From fed9120d06738abd61b2f465c54a96e22807d959 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 14:39:24 +0200 Subject: [PATCH 06/21] align order of properties --- .../hs/booking/item/HsBookingItemEntity.java | 8 +++---- .../601-booking-item/6010-hs-booking-item.sql | 2 +- ...HsBookingItemControllerAcceptanceTest.java | 6 ++--- .../HsBookingItemEntityPatcherUnitTest.java | 2 +- .../item/HsBookingItemEntityUnitTest.java | 2 +- ...sBookingItemRepositoryIntegrationTest.java | 22 +++++++++---------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index cf29c709..04d74012 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -49,8 +49,8 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { private static Stringify stringify = stringify(HsBookingItemEntity.class) .withProp(e -> e.getDebitor().toShortString()) - .withProp(HsBookingItemEntity::getCaption) .withProp(e -> e.getValidity().asString()) + .withProp(HsBookingItemEntity::getCaption) .withProp(HsBookingItemEntity::getResources) .quotedValues(false); @@ -61,9 +61,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Version private int version; - @Column(name = "caption") - private String caption; - @ManyToOne(optional = false) @JoinColumn(name = "debitoruuid") private HsOfficeDebitorEntity debitor; @@ -73,6 +70,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Column(name = "validity", columnDefinition = "daterange") private Range validity = Range.emptyRange(LocalDate.class); + @Column(name = "caption") + private String caption; + @Builder.Default @Type(JsonType.class) @Column(columnDefinition = "resources") diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql index ec671494..e42fa2e1 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql @@ -9,8 +9,8 @@ create table if not exists hs_booking_item uuid uuid unique references RbacObject (uuid), version int not null default 0, debitorUuid uuid not null references hs_office_debitor(uuid), - caption varchar(80) not null, validity daterange not null, + caption varchar(80) not null, resources jsonb not null ); --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 25d4478f..0ce06015 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -5,8 +5,8 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; -import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; -import net.hostsharing.test.JpaAttempt; +import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,7 +21,7 @@ import java.util.Map; import java.util.UUID; import static java.util.Map.entry; -import static net.hostsharing.test.JsonMatcher.lenientlyEquals; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.startsWith; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index a90baf38..3f2f426d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -4,7 +4,7 @@ import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.test.PatchUnitTestBase; +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; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java index c5513fa9..ce85e7fc 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java @@ -28,7 +28,7 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, some caption, [2020-01-01,2031-01-01), {CPUs=2, HDD-storage=2048, SSD-storage=512})"); + assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, [2020-01-01,2031-01-01), some caption, {CPUs=2, HDD-storage=2048, SSD-storage=512})"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index 171535a4..ea3558fd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -3,11 +3,11 @@ package net.hostsharing.hsadminng.hs.booking.item; import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; -import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.test.Array; -import net.hostsharing.test.JpaAttempt; +import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,8 +27,8 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.test.Array.fromFormatted; -import static net.hostsharing.test.JpaAttempt.attempt; +import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -156,9 +156,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212, some CloudServer, [2023-01-15,2024-04-15), {CPUs=2, HDD-storage=1024})", - "HsBookingItemEntity(D-1000212, some ManagedServer, [2022-10-01,), {CPUs=2, SDD-storage=512})", - "HsBookingItemEntity(D-1000212, some Whatever, [2024-04-01,), {CPUs=1, HDD-storage=2048, SDD-storage=512})"); + "HsBookingItemEntity(D-1000212, [2023-01-15,2024-04-15), some CloudServer, {CPUs=2, HDD-storage=1024})", + "HsBookingItemEntity(D-1000212, [2022-10-01,), some ManagedServer, {CPUs=2, SDD-storage=512})", + "HsBookingItemEntity(D-1000212, [2024-04-01,), some Whatever, {CPUs=1, HDD-storage=2048, SDD-storage=512})"); } @Test @@ -173,9 +173,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, some CloudServer, [2023-01-15,2024-04-15), {CPUs=2, HDD-storage=1024})", - "HsBookingItemEntity(D-1000111, some ManagedServer, [2022-10-01,), {CPUs=2, SDD-storage=512})", - "HsBookingItemEntity(D-1000111, some Whatever, [2024-04-01,), {CPUs=1, HDD-storage=2048, SDD-storage=512})"); + "HsBookingItemEntity(D-1000111, [2023-01-15,2024-04-15), some CloudServer, {CPUs=2, HDD-storage=1024})", + "HsBookingItemEntity(D-1000111, [2022-10-01,), some ManagedServer, {CPUs=2, SDD-storage=512})", + "HsBookingItemEntity(D-1000111, [2024-04-01,), some Whatever, {CPUs=1, HDD-storage=2048, SDD-storage=512})"); } } -- 2.39.5 From 2db91e7816499d8dc9622cf01a6b68f0daea5790 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 14:53:14 +0200 Subject: [PATCH 07/21] cleanup --- .../item/HsBookingItemEntityPatcherUnitTest.java | 4 ++-- .../item/HsBookingItemRepositoryIntegrationTest.java | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 3f2f426d..8bebf69f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -70,8 +70,8 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< } @Override - protected HsBookingItemEntityPatcher createPatcher(final HsBookingItemEntity sepaMandate) { - return new HsBookingItemEntityPatcher(sepaMandate); + protected HsBookingItemEntityPatcher createPatcher(final HsBookingItemEntity bookingItem) { + return new HsBookingItemEntityPatcher(bookingItem); } @Override diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index ea3558fd..518a0f6b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -164,7 +164,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup @Test public void normalUser_canViewOnlyRelatedBookingItems() { // given: - context("bankaccount-admin@FirstGmbH.example.com"); + context("person-FirbySusan@example.com"); final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); // when: @@ -212,15 +212,6 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup assertThat(found).isNotEmpty().get().isNotSameAs(saved) .extracting(Object::toString).isEqualTo(saved.toString()); } - - private void assertThatBookingItemIsVisibleForUserWithRole( - final HsBookingItemEntity entity, - final String assumedRoles) { - jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", assumedRoles); - assertThatBookingItemActuallyInDatabase(entity); - }).assertSuccessful(); - } } @Nested -- 2.39.5 From 9d0dc358f0cd85b4fe2795c7515e3a88970b52f0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Apr 2024 15:34:06 +0200 Subject: [PATCH 08/21] implement proper identity view with debitorNumber:caption --- .../hs/booking/item/HsBookingItemEntity.java | 6 +++++- .../6013-hs-booking-item-rbac.sql | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 04d74012..3527d9d1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -117,7 +117,11 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { public static RbacView rbac() { return rbacViewFor("bookingItem", HsBookingItemEntity.class) - .withIdentityView(SQL.projection("caption")) // FIXME: use memberNumber:caption + .withIdentityView(SQL.query(""" + SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName + FROM hs_booking_item i + JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid + """)) .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "validity", "resources") diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql index aa138789..590fef50 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql @@ -107,14 +107,14 @@ do language plpgsql $$ WHERE type in ('DEBITOR') -- TODO.rbac: currently manually patched, needs to be generated LOOP call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', 'hs_booking_item'), - hsOfficeRelationADMIN(row)); + createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + hsOfficeRelationADMIN(row)); END LOOP; END; $$; /** - Adds hs_booking_item INSERT permission to specified roleNSERT permission to specified role of new hs_office_relation rows. + Adds hs_booking_item INSERT permission to specified role of new hs_office_relation rows. */ create or replace function hs_booking_item_hs_office_relation_insert_tf() returns trigger @@ -175,10 +175,12 @@ create trigger hs_booking_item_insert_permission_check_tg --changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityViewFromProjection('hs_booking_item', - $idName$ - caption - $idName$); + call generateRbacIdentityViewFromQuery('hs_booking_item', + $idName$ + SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName + FROM hs_booking_item i + JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid + $idName$); --// -- ============================================================================ @@ -186,7 +188,7 @@ call generateRbacIdentityViewFromProjection('hs_booking_item', -- ---------------------------------------------------------------------------- call generateRbacRestrictedView('hs_booking_item', $orderBy$ - caption + validity $orderBy$, $updates$ version = new.version, -- 2.39.5 From 89ef2d9126ec6961592f9e2a4acbfe158abc9923 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 14 Apr 2024 11:58:57 +0200 Subject: [PATCH 09/21] implement patching BookingItem.resources --- .../hs/booking/item/HsBookingItemEntity.java | 18 ++++++---- .../item/HsBookingItemEntityPatcher.java | 19 +++++++--- .../HsOfficeCoopAssetsTransactionEntity.java | 1 - ...OfficeCoopSharesTransactionController.java | 2 -- .../HsOfficeCoopSharesTransactionEntity.java | 1 - ...HsBookingItemControllerAcceptanceTest.java | 3 +- .../HsBookingItemEntityPatcherUnitTest.java | 33 +++++++++-------- .../item/HsBookingItemEntityUnitTest.java | 2 +- ...sBookingItemRepositoryIntegrationTest.java | 35 ++++++++++--------- ...ceCoopSharesTransactionEntityUnitTest.java | 3 -- 10 files changed, 65 insertions(+), 52 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 3527d9d1..5b979216 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -26,7 +26,7 @@ import java.util.UUID; import java.util.function.BinaryOperator; import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.joining; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; @@ -51,9 +51,18 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { .withProp(e -> e.getDebitor().toShortString()) .withProp(e -> e.getValidity().asString()) .withProp(HsBookingItemEntity::getCaption) - .withProp(HsBookingItemEntity::getResources) + .withProp(HsBookingItemEntity::getResourcesAsString) .quotedValues(false); + private String getResourcesAsString() { + return "{ " + + ( + resources.keySet().stream().sorted() + .map(k -> k + ": " + resources.get(k))) + .collect(joining(", ") + ) + " }"; + } + @Id @GeneratedValue private UUID uuid; @@ -78,11 +87,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Column(columnDefinition = "resources") private Map resources = new TreeMap<>(); - public Map getResources() { - return resources.entrySet().stream() - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, HsBookingItemEntity::thereIsOnlyOneValuePerKey, TreeMap::new)); - } - public void setValidFrom(final LocalDate validFrom) { setValidity(toPostgresDateRange(validFrom, getValidTo())); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 865e16c8..491fea11 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -4,10 +4,11 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBook import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.OptionalFromJson; +import org.apache.commons.lang3.tuple.ImmutablePair; +import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import static java.util.Arrays.stream; @@ -38,16 +39,24 @@ public class HsBookingItemEntityPatcher implements EntityPatcher objectToMap(final Object obj) { - final var map = stream(obj.getClass().getDeclaredFields()) + return stream(obj.getClass().getDeclaredFields()) .map(field -> { try { field.setAccessible(true); - return Map.entry(field.getName(), field.get(obj)); + return new ImmutablePair<>(field.getName(), field.get(obj)); } catch (final IllegalAccessException exc) { throw new RuntimeException(exc); } }) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return map; + .reduce( + new HashMap<>(), + (map, pair) -> { + map.put(pair.getKey(), pair.getValue()); + return map; + }, + (map1, map2) -> { + map1.putAll(map2); + return map1; + }); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index c22455a4..4ec6685d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -18,7 +18,6 @@ import jakarta.persistence.*; import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Optional; import java.util.UUID; import static java.util.Optional.ofNullable; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index e053843f..9a3295a2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -2,9 +2,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares; import jakarta.persistence.EntityNotFoundException; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; import net.hostsharing.hsadminng.mapper.Mapper; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java index c9f334e6..8604ec16 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java @@ -6,7 +6,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.errors.DisplayName; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 0ce06015..da88a87e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -148,6 +148,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000111) + .filter(item -> item.getCaption().equals("some CloudServer")) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -192,8 +193,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000313) + .filter(item -> item.getCaption().equals("some CloudServer")) .findAny().orElseThrow().getUuid(); - generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "booking-item-of-debitor-1000313"); RestAssured // @formatter:off .given() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 8bebf69f..c29edc4c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -35,12 +35,17 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< private static final LocalDate PATCHED_VALID_FROM = LocalDate.parse("2022-10-30"); private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); - private static final ArbitraryBookingResourcesJsonResource PATCHED_RESOURCES = new ArbitraryBookingResourcesJsonResource() { - int cpus = 2; - int sddStorage = 256; - int hddStorage = 2048; + private static final ArbitraryBookingResourcesJsonResource INITIAL_RESOURCES = new ArbitraryBookingResourcesJsonResource() { + Integer cpus = 1; + Integer hddStorage = 1024; }; - private static final String PATCHED_CAPTION = "caption-patched"; + private static final ArbitraryBookingResourcesJsonResource PATCHED_RESOURCES = new ArbitraryBookingResourcesJsonResource() { + Integer cpus = 2; + Integer sddStorage = 256; + Integer hddStorage = null; + }; + private static final String INITIAL_CAPTION = "initial caption"; + private static final String PATCHED_CAPTION = "patched caption"; @Mock private EntityManager em; @@ -58,8 +63,8 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< final var entity = new HsBookingItemEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); entity.setDebitor(TEST_DEBITOR); - entity.setResources(objectToMap(PATCHED_RESOURCES)); - entity.setCaption(PATCHED_CAPTION); + entity.setResources(objectToMap(INITIAL_RESOURCES)); + entity.setCaption(INITIAL_CAPTION); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); return entity; } @@ -82,13 +87,13 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< HsBookingItemPatchResource::setCaption, PATCHED_CAPTION, HsBookingItemEntity::setCaption), -// FIXME -// new JsonNullableProperty<>( -// "resources", -// HsBookingItemPatchResource::setResources, -// PATCHED_RESOURCES, -// HsBookingItemEntity::setResources, -// objectToMap(PATCHED_RESOURCES)), + new SimpleProperty<>( + "resources", + HsBookingItemPatchResource::setResources, + PATCHED_RESOURCES, + HsBookingItemEntity::setResources, + objectToMap(PATCHED_RESOURCES)) + .notNullable(), new JsonNullableProperty<>( "validfrom", HsBookingItemPatchResource::setValidFrom, diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java index ce85e7fc..20bad4eb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java @@ -28,7 +28,7 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, [2020-01-01,2031-01-01), some caption, {CPUs=2, HDD-storage=2048, SSD-storage=512})"); + assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index 518a0f6b..ddf62936 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -109,16 +109,16 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_booking_item#somenewbookingitem:ADMIN", - "hs_booking_item#somenewbookingitem:OWNER", - "hs_booking_item#somenewbookingitem:TENANT")); + "hs_booking_item#D-1000111:some new booking item:ADMIN", + "hs_booking_item#D-1000111:some new booking item:OWNER", + "hs_booking_item#D-1000111:some new booking item:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // insert+delete - "{ grant perm:hs_booking_item#somenewbookingitem:DELETE to role:global#global:ADMIN by system and assume }", + "{ grant perm:hs_booking_item#D-1000111:some new booking item:DELETE to role:global#global:ADMIN by system and assume }", // owner @@ -126,12 +126,12 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // tenant - "{ grant perm:hs_booking_item#somenewbookingitem:SELECT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", - "{ grant perm:hs_booking_item#somenewbookingitem:UPDATE to role:hs_booking_item#somenewbookingitem:OWNER by system and assume }", - "{ grant role:hs_booking_item#somenewbookingitem:ADMIN to role:hs_booking_item#somenewbookingitem:OWNER by system and assume }", - "{ grant role:hs_booking_item#somenewbookingitem:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", - "{ grant role:hs_booking_item#somenewbookingitem:TENANT to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", - "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", + "{ grant perm:hs_booking_item#D-1000111:some new booking item:SELECT to role:hs_booking_item#D-1000111:some new booking item:TENANT by system and assume }", + "{ grant perm:hs_booking_item#D-1000111:some new booking item:UPDATE to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", + "{ grant role:hs_booking_item#D-1000111:some new booking item:ADMIN to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", + "{ grant role:hs_booking_item#D-1000111:some new booking item:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", + "{ grant role:hs_booking_item#D-1000111:some new booking item:TENANT to role:hs_booking_item#D-1000111:some new booking item:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_item#D-1000111:some new booking item:TENANT by system and assume }", null)); } @@ -148,7 +148,8 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup public void globalAdmin_withoutAssumedRole_canViewAllBookingItemsOfArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream().findAny().orElseThrow().getUuid(); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream() + .findAny().orElseThrow().getUuid(); // when final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); @@ -156,9 +157,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212, [2023-01-15,2024-04-15), some CloudServer, {CPUs=2, HDD-storage=1024})", - "HsBookingItemEntity(D-1000212, [2022-10-01,), some ManagedServer, {CPUs=2, SDD-storage=512})", - "HsBookingItemEntity(D-1000212, [2024-04-01,), some Whatever, {CPUs=1, HDD-storage=2048, SDD-storage=512})"); + "HsBookingItemEntity(D-1000212, [2022-10-01,), some ManagedServer, { CPUs: 2, SDD-storage: 512 })", + "HsBookingItemEntity(D-1000212, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD-storage: 1024 })", + "HsBookingItemEntity(D-1000212, [2024-04-01,), some Whatever, { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 })"); } @Test @@ -173,9 +174,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, [2023-01-15,2024-04-15), some CloudServer, {CPUs=2, HDD-storage=1024})", - "HsBookingItemEntity(D-1000111, [2022-10-01,), some ManagedServer, {CPUs=2, SDD-storage=512})", - "HsBookingItemEntity(D-1000111, [2024-04-01,), some Whatever, {CPUs=1, HDD-storage=2048, SDD-storage=512})"); + "HsBookingItemEntity(D-1000111, [2022-10-01,), some ManagedServer, { CPUs: 2, SDD-storage: 512 })", + "HsBookingItemEntity(D-1000111, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD-storage: 1024 })", + "HsBookingItemEntity(D-1000111, [2024-04-01,), some Whatever, { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 })"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java index 44ade22c..08a2718d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityUnitTest.java @@ -1,10 +1,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType; import org.junit.jupiter.api.Test; -import java.math.BigDecimal; import java.time.LocalDate; import static net.hostsharing.hsadminng.hs.office.membership.TestHsMembership.TEST_MEMBERSHIP; -- 2.39.5 From 73d0ef8f787f4f3662801d792864c5c173691c75 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 14 Apr 2024 13:07:24 +0200 Subject: [PATCH 10/21] introduce PatchableMap and make HsBookingItemEntity.resources readonly --- .../hs/booking/item/HsBookingItemEntity.java | 53 ++++++++++--------- .../hsadminng/mapper/PatchableMap.java | 42 +++++++++++++++ .../HsBookingItemEntityPatcherUnitTest.java | 5 +- ...sBookingItemRepositoryIntegrationTest.java | 20 +++---- 4 files changed, 82 insertions(+), 38 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 5b979216..a46b3d60 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import io.hypersistence.utils.hibernate.type.json.JsonType; import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType; import io.hypersistence.utils.hibernate.type.range.Range; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -10,31 +11,44 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; -import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; import java.io.IOException; import java.time.LocalDate; import java.util.Map; import java.util.TreeMap; import java.util.UUID; -import java.util.function.BinaryOperator; import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.mapper.PatchableMap.mapToString; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @@ -51,18 +65,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { .withProp(e -> e.getDebitor().toShortString()) .withProp(e -> e.getValidity().asString()) .withProp(HsBookingItemEntity::getCaption) - .withProp(HsBookingItemEntity::getResourcesAsString) + .withProp(e -> mapToString(e.getResources())) .quotedValues(false); - private String getResourcesAsString() { - return "{ " + - ( - resources.keySet().stream().sorted() - .map(k -> k + ": " + resources.get(k))) - .collect(joining(", ") - ) + " }"; - } - @Id @GeneratedValue private UUID uuid; @@ -83,6 +88,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { private String caption; @Builder.Default + @Setter(AccessLevel.NONE) @Type(JsonType.class) @Column(columnDefinition = "resources") private Map resources = new TreeMap<>(); @@ -103,7 +109,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } - @Override public String toString() { return stringify.apply(this); @@ -115,17 +120,13 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { ":" + caption; } - private static BinaryOperator thereIsOnlyOneValuePerKey(Object o, Object o1) { - return (a, b) -> a; - } - public static RbacView rbac() { return rbacViewFor("bookingItem", HsBookingItemEntity.class) .withIdentityView(SQL.query(""" - SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName - FROM hs_booking_item i - JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid - """)) + SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName + FROM hs_booking_item i + JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid + """)) .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "validity", "resources") diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java new file mode 100644 index 00000000..45669670 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java @@ -0,0 +1,42 @@ +package net.hostsharing.hsadminng.mapper; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import jakarta.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import static java.util.stream.Collectors.joining; + +public class PatchableMap { + + @SafeVarargs + public static Map patchableMap(final ImmutablePair... entries) { + final var map = new HashMap(); + Arrays.stream(entries).forEach(r -> map.put(r.getKey(), r.getValue())); + return map; + } + + @NotNull + public static ImmutablePair entry(final String key, final Object value) { + return new ImmutablePair<>(key, value); + } + + public static BiConsumer> assignMap = (HsBookingItemEntity i, Map r) -> { + i.getResources().clear(); + i.getResources().putAll(r); + }; + + public static String mapToString(final Map resources) { + return "{ " + + ( + resources.keySet().stream().sorted() + .map(k -> k + ": " + resources.get(k))) + .collect(joining(", ") + ) + + " }"; + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index c29edc4c..afc825e0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -4,6 +4,7 @@ import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.mapper.PatchableMap; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -63,7 +64,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< final var entity = new HsBookingItemEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); entity.setDebitor(TEST_DEBITOR); - entity.setResources(objectToMap(INITIAL_RESOURCES)); + entity.getResources().putAll(objectToMap(INITIAL_RESOURCES)); entity.setCaption(INITIAL_CAPTION); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); return entity; @@ -91,7 +92,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< "resources", HsBookingItemPatchResource::setResources, PATCHED_RESOURCES, - HsBookingItemEntity::setResources, + PatchableMap.assignMap, objectToMap(PATCHED_RESOURCES)) .notNullable(), new JsonNullableProperty<>( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index ddf62936..e90b0cb6 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -22,9 +22,9 @@ import jakarta.servlet.http.HttpServletRequest; import java.time.LocalDate; import java.util.Arrays; import java.util.List; -import java.util.Map; -import static java.util.Map.entry; +import static net.hostsharing.hsadminng.mapper.PatchableMap.entry; +import static net.hostsharing.hsadminng.mapper.PatchableMap.patchableMap; 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; @@ -186,18 +186,18 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup @Test public void hostsharingAdmin_canUpdateArbitraryBookingItem() { // given - final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + final var givenBookingItemUuid = givenSomeTemporaryBookingItem(1000111).getUuid(); // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - givenBookingItem.setResources(Map.ofEntries( - entry("CPUs", 2), - entry("SSD-storage", 512), - entry("HDD-storage", 2048))); - givenBookingItem.setValidity(Range.closedOpen( + final var foundBookingItem = em.find(HsBookingItemEntity.class, givenBookingItemUuid); + foundBookingItem.getResources().put("CPUs", 2); + foundBookingItem.getResources().remove("SSD-storage"); + foundBookingItem.getResources().put("HSD-storage", 2048); + foundBookingItem.setValidity(Range.closedOpen( LocalDate.parse("2019-05-17"), LocalDate.parse("2023-01-01"))); - return toCleanup(bookingItemRepo.save(givenBookingItem)); + return toCleanup(bookingItemRepo.save(foundBookingItem)); }); // then @@ -312,7 +312,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup .caption("some temp booking item") .validity(Range.closedOpen( LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) - .resources(Map.ofEntries( + .resources(patchableMap( entry("CPUs", 1), entry("SSD-storage", 256))) .build(); -- 2.39.5 From 2ebbea1c24a21f5abea2250b2572bb497ece6f71 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 14 Apr 2024 16:11:41 +0200 Subject: [PATCH 11/21] WIP --- .../hs/booking/item/HsBookingItemEntity.java | 22 +++- .../item/HsBookingItemEntityPatcher.java | 8 +- .../hsadminng/mapper/PatchMap.java | 30 +++++ .../hsadminng/mapper/PatchableMap.java | 42 ------- .../hsadminng/mapper/PatchableMapWrapper.java | 107 ++++++++++++++++++ ...HsBookingItemControllerAcceptanceTest.java | 14 ++- .../HsBookingItemEntityPatcherUnitTest.java | 3 +- ...sBookingItemRepositoryIntegrationTest.java | 6 +- 8 files changed, 171 insertions(+), 61 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index a46b3d60..828ffe0f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -11,6 +11,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; @@ -25,15 +26,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.time.LocalDate; +import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.mapper.PatchableMap.mapToString; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; @@ -65,7 +67,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { .withProp(e -> e.getDebitor().toShortString()) .withProp(e -> e.getValidity().asString()) .withProp(HsBookingItemEntity::getCaption) - .withProp(e -> mapToString(e.getResources())) + .withProp(HsBookingItemEntity::getResources) .quotedValues(false); @Id @@ -91,7 +93,10 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Setter(AccessLevel.NONE) @Type(JsonType.class) @Column(columnDefinition = "resources") - private Map resources = new TreeMap<>(); + private Map resources = new HashMap<>(); + + @Transient + private PatchableMapWrapper resourcesWrapper; public void setValidFrom(final LocalDate validFrom) { setValidity(toPostgresDateRange(validFrom, getValidTo())); @@ -109,6 +114,17 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } + public PatchableMapWrapper getResources() { + if ( resourcesWrapper == null ) { + resourcesWrapper = new PatchableMapWrapper(resources); + } + return resourcesWrapper; + } + + public void putResources(Map entries) { + resourcesWrapper.assign(entries); + } + @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 491fea11..39fb863d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -25,19 +25,13 @@ public class HsBookingItemEntityPatcher implements EntityPatcher setResources(entity, r)); + .ifPresent(r -> entity.putResources(objectToMap(resource.getResources()))); OptionalFromJson.of(resource.getValidFrom()) .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) .ifPresent(entity::setValidTo); } - private static void setResources( - final HsBookingItemEntity entity, - final ArbitraryBookingResourcesJsonResource arbitraryBookingResourcesJsonResource) { - entity.getResources().putAll(objectToMap(arbitraryBookingResourcesJsonResource)); - } - static Map objectToMap(final Object obj) { return stream(obj.getClass().getDeclaredFields()) .map(field -> { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java new file mode 100644 index 00000000..abcc9c4f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.mapper; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * This is a map which can take key-value-pairs where the value can be null + * thus JSON nullable object structures from HTTP PATCH can be represented. + */ +public class PatchMap extends TreeMap { + + public PatchMap(final ImmutablePair[] entries) { + stream(entries).forEach(r -> put(r.getKey(), r.getValue())); + } + +// @SafeVarargs +// public static Map patchMap(final ImmutablePair... entries) { +// return new PatchMap(entries); +// } +// +// @NotNull +// public static ImmutablePair entry(final String key, final Object value) { +// return new ImmutablePair<>(key, value); +// } +} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java deleted file mode 100644 index 45669670..00000000 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.mapper; - -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import org.apache.commons.lang3.tuple.ImmutablePair; - -import jakarta.validation.constraints.NotNull; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; - -import static java.util.stream.Collectors.joining; - -public class PatchableMap { - - @SafeVarargs - public static Map patchableMap(final ImmutablePair... entries) { - final var map = new HashMap(); - Arrays.stream(entries).forEach(r -> map.put(r.getKey(), r.getValue())); - return map; - } - - @NotNull - public static ImmutablePair entry(final String key, final Object value) { - return new ImmutablePair<>(key, value); - } - - public static BiConsumer> assignMap = (HsBookingItemEntity i, Map r) -> { - i.getResources().clear(); - i.getResources().putAll(r); - }; - - public static String mapToString(final Map resources) { - return "{ " - + ( - resources.keySet().stream().sorted() - .map(k -> k + ": " + resources.get(k))) - .collect(joining(", ") - ) - + " }"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java new file mode 100644 index 00000000..beb6cc5b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -0,0 +1,107 @@ +package net.hostsharing.hsadminng.mapper; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import jakarta.validation.constraints.NotNull; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +/** This class wraps another (usually persistent) map and + * supports applying `PatchMap` as well as a toString method with stable entry order. + */ +public class PatchableMapWrapper implements Map { + + private final Map delegate; + + public PatchableMapWrapper(final Map map) { + delegate = map; + } + + @NotNull + public static ImmutablePair entry(final String key, final Object value) { + return new ImmutablePair<>(key, value); + } + + public void assign(final Map entries) { + delegate.clear(); + delegate.putAll(entries); + } + + public String toString() { + return "{ " + + ( + keySet().stream().sorted() + .map(k -> k + ": " + get(k))) + .collect(joining(", ") + ) + + " }"; + } + + // --- below just delegating methods -------------------------------- + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return delegate.containsValue(value); + } + + @Override + public Object get(final Object key) { + return delegate.get(key); + } + + @Override + public Object put(final String key, final Object value) { + return delegate.put(key, value); + } + + @Override + public Object remove(final Object key) { + return delegate.remove(key); + } + + @Override + public void putAll(final Map m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index da88a87e..f5dafb11 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -233,8 +233,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2020-06-05", "validTo": "2022-12-31", "resources": { - "CPUs": "2", - "SSD-storage": "512" + "CPUs": "4", + "HDD_storage": null, + "SSD-storage": "4096" } } """) @@ -249,7 +250,10 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some test-booking", "validFrom": "2020-06-05", "validTo": "2022-12-31", - "resources": null + "resources": { + "CPUs": "2", + "SSD-storage": "2048" + } } """)); // @formatter:on @@ -290,7 +294,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some test-booking", "validFrom": "2022-11-01", "validTo": "2022-12-31", - "resources": null + "resources": { + "CPUs": 5 + } } """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index afc825e0..5163305f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -4,7 +4,6 @@ import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.mapper.PatchableMap; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -92,7 +91,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< "resources", HsBookingItemPatchResource::setResources, PATCHED_RESOURCES, - PatchableMap.assignMap, + HsBookingItemEntity::putResources, objectToMap(PATCHED_RESOURCES)) .notNullable(), new JsonNullableProperty<>( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index e90b0cb6..e1558f95 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -22,9 +22,9 @@ import jakarta.servlet.http.HttpServletRequest; import java.time.LocalDate; import java.util.Arrays; import java.util.List; +import java.util.Map; -import static net.hostsharing.hsadminng.mapper.PatchableMap.entry; -import static net.hostsharing.hsadminng.mapper.PatchableMap.patchableMap; +import static java.util.Map.entry; 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; @@ -312,7 +312,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup .caption("some temp booking item") .validity(Range.closedOpen( LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) - .resources(patchableMap( + .resources(Map.ofEntries( entry("CPUs", 1), entry("SSD-storage", 256))) .build(); -- 2.39.5 From 09405eb5496996a0c24f514b0a9c29b34f58b89c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 15 Apr 2024 07:36:54 +0200 Subject: [PATCH 12/21] fix expected resources in acceptance test --- .../HsBookingItemControllerAcceptanceTest.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index f5dafb11..7579e23c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -66,26 +66,25 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .then().log().all().assertThat() .statusCode(200) .contentType("application/json") - .log().all() .body("", lenientlyEquals(""" [ { "caption": "some ManagedServer", "validFrom": "2022-10-01", "validTo": null, - "resources": null + "resources": { CPUs: 2, SDD-storage: 512 } }, { "caption": "some CloudServer", "validFrom": "2023-01-15", "validTo": "2024-04-14", - "resources": null + "resources": { CPUs: 2, HDD-storage: 1024 } }, { "caption": "some Whatever", "validFrom": "2024-04-01", "validTo": null, - "resources": null + "resources": { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 } } ] """)); @@ -110,9 +109,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { "debitorUuid": "%s", "caption": "some new booking", - "resources": { - "something": 12 - }, + "resources": { "something": 12 }, "validFrom": "2022-10-13" } """.formatted(givenDebitor.getUuid())) @@ -127,7 +124,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new booking", "validFrom": "2022-10-13", "validTo": null, - "resources": null + "resources": { "something": 12 } } """)) .header("Location", startsWith("http://localhost")) @@ -165,7 +162,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some CloudServer", "validFrom": "2023-01-15", "validTo": "2024-04-14", - "resources": null + "resources": { CPUs: 2, HDD-storage: 1024 } } """)); // @formatter:on } @@ -210,7 +207,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some CloudServer", "validFrom": "2023-01-15", "validTo": "2024-04-14", - "resources": null + "resources": { CPUs: 2, HDD-storage: 1024 } } """)); // @formatter:on } -- 2.39.5 From 3e2775a8a010b13e744ed29caf18e5157717b650 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 15 Apr 2024 15:06:18 +0200 Subject: [PATCH 13/21] upgrade io.openapiprocessor:openapi-processor-spring to 2024.2 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 32f42cd6..4867a81c 100644 --- a/build.gradle +++ b/build.gradle @@ -151,12 +151,13 @@ openapiProcessor { } springHsBooking { processorName 'spring' - processor 'io.openapiprocessor:openapi-processor-spring:2022.5' + processor 'io.openapiprocessor:openapi-processor-spring:2024.2' apiPath "$projectDir/src/main/resources/api-definition/hs-booking/hs-booking.yaml" mapping "$projectDir/src/main/resources/api-definition/hs-booking/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true + // generateAliasAsModel true } } sourceSets.main.java.srcDir 'build/generated/sources/openapi' -- 2.39.5 From 3a894ed72e5b907645a220980ff24a0fbf731c67 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 15 Apr 2024 15:15:27 +0200 Subject: [PATCH 14/21] patchable BookingResources --- .../booking/item/HsBookingItemController.java | 3 + .../hs/booking/item/HsBookingItemEntity.java | 4 +- .../item/HsBookingItemEntityPatcher.java | 11 ++- .../hsadminng/mapper/PatchMap.java | 18 ++-- .../hsadminng/mapper/PatchableMapWrapper.java | 13 ++- .../hs-booking/hs-booking-item-schemas.yaml | 58 +++++++++++-- .../hs-booking-items-with-uuid.yaml | 32 +++---- .../hs-booking/hs-booking-items.yaml | 24 +++--- .../api-definition/hs-booking/hs-booking.yaml | 4 +- .../6018-hs-booking-item-test-data.sql | 6 +- ...HsBookingItemControllerAcceptanceTest.java | 85 +++++++++++-------- .../HsBookingItemEntityPatcherUnitTest.java | 33 ++++--- ...sBookingItemRepositoryIntegrationTest.java | 12 +-- 13 files changed, 196 insertions(+), 107 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index b74e7e0c..06926266 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; @@ -122,7 +123,9 @@ public class HsBookingItemController implements HsBookingItemsApi { } }; + @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); + entity.putResources((Map) resource.getResources()); }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 828ffe0f..7a846f46 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.time.LocalDate; import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; import java.util.UUID; import static java.util.Optional.ofNullable; @@ -122,6 +121,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { } public void putResources(Map entries) { + if ( resourcesWrapper == null ) { + resourcesWrapper = new PatchableMapWrapper(resources); + } resourcesWrapper.assign(entries); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 39fb863d..1905967b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -1,6 +1,5 @@ package net.hostsharing.hsadminng.hs.booking.item; -import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.OptionalFromJson; @@ -25,7 +24,7 @@ public class HsBookingItemEntityPatcher implements EntityPatcher entity.putResources(objectToMap(resource.getResources()))); + .ifPresent(r -> entity.getResources().patch(objectToMap(resource.getResources()))); OptionalFromJson.of(resource.getValidFrom()) .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) @@ -33,6 +32,9 @@ public class HsBookingItemEntityPatcher implements EntityPatcher objectToMap(final Object obj) { + if (obj instanceof Map) { + return toKeyValueMap(obj); + } return stream(obj.getClass().getDeclaredFields()) .map(field -> { try { @@ -53,4 +55,9 @@ public class HsBookingItemEntityPatcher implements EntityPatcher toKeyValueMap(final Object obj) { + return (Map) obj; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java index abcc9c4f..74a36bfa 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java @@ -18,13 +18,13 @@ public class PatchMap extends TreeMap { stream(entries).forEach(r -> put(r.getKey(), r.getValue())); } -// @SafeVarargs -// public static Map patchMap(final ImmutablePair... entries) { -// return new PatchMap(entries); -// } -// -// @NotNull -// public static ImmutablePair entry(final String key, final Object value) { -// return new ImmutablePair<>(key, value); -// } + @SafeVarargs + public static Map patchMap(final ImmutablePair... entries) { + return new PatchMap(entries); + } + + @NotNull + public static ImmutablePair entry(final String key, final Object value) { + return new ImmutablePair<>(key, value); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java index beb6cc5b..678a68cd 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -4,12 +4,9 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import jakarta.validation.constraints.NotNull; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.TreeMap; -import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; /** This class wraps another (usually persistent) map and @@ -33,6 +30,16 @@ public class PatchableMapWrapper implements Map { delegate.putAll(entries); } + public void patch(final Map patch) { + patch.forEach((key, value) -> { + if (value == null) { + remove(key); + } else { + put(key, value); + } + }); + } + public String toString() { return "{ " + ( diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index e06a5ffd..7424a46e 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -18,7 +18,7 @@ components: type: string format: date resources: - $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + $ref: '#/components/schemas/BookingResources' required: - uuid - debitor @@ -41,7 +41,7 @@ components: format: date nullable: true resources: - $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + $ref: '#/components/schemas/BookingResources' HsBookingItemInsert: type: object @@ -64,7 +64,7 @@ components: format: date nullable: true resources: - $ref: '#/components/schemas/ArbitraryBookingResourcesJson' + $ref: '#/components/schemas/BookingResources' required: - caption - debitorUuid @@ -72,7 +72,53 @@ components: - resources additionalProperties: false - ArbitraryBookingResourcesJson: + BookingResourcesDoesNotWork: type: object - description: An object containing arbitrary JSON - additionalProperties: true + x-javaType: 'java.util.Map' + additionalProperties: + type: object + + BookingResources: + anyOf: + - $ref: '#/components/schemas/ManagedServerBookingResources' + - $ref: '#/components/schemas/ManagedWebspaceBookingResources' + + ManagedServerBookingResources: + type: object + properties: + caption: + type: string + minLength: 3 + maxLength: + nullable: false + CPU: + type: integer + minimum: 1 + maximum: 16 + SSD: + type: integer + minimum: 16 + maximum: 4096 + HDD: + type: integer + minimum: 16 + maximum: 4096 + additionalProperties: false + + ManagedWebspaceBookingResources: + type: object + properties: + disk: + type: integer + minimum: 1 + maximum: 16 + SSD: + type: integer + minimum: 16 + maximum: 4096 + HDD: + type: integer + minimum: 16 + maximum: 4096 + additionalProperties: false + diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml index 99a8803a..3d7567c8 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items-with-uuid.yaml @@ -4,8 +4,8 @@ get: description: 'Fetch a single booking item its uuid, if visible for the current subject.' operationId: getBookingItemByUuid parameters: - - $ref: './auth.yaml#/components/parameters/currentUser' - - $ref: './auth.yaml#/components/parameters/assumedRoles' + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: bookingItemUuid in: path required: true @@ -19,12 +19,12 @@ get: content: 'application/json': schema: - $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' "401": - $ref: './error-responses.yaml#/components/responses/Unauthorized' + $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": - $ref: './error-responses.yaml#/components/responses/Forbidden' + $ref: 'error-responses.yaml#/components/responses/Forbidden' patch: tags: @@ -32,8 +32,8 @@ patch: description: 'Updates a single booking item identified by its uuid, if permitted for the current subject.' operationId: patchBookingItem parameters: - - $ref: './auth.yaml#/components/parameters/currentUser' - - $ref: './auth.yaml#/components/parameters/assumedRoles' + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: bookingItemUuid in: path required: true @@ -44,18 +44,18 @@ patch: content: 'application/json': schema: - $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemPatch' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemPatch' responses: "200": description: OK content: 'application/json': schema: - $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' "401": - $ref: './error-responses.yaml#/components/responses/Unauthorized' + $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": - $ref: './error-responses.yaml#/components/responses/Forbidden' + $ref: 'error-responses.yaml#/components/responses/Forbidden' delete: tags: @@ -63,8 +63,8 @@ delete: description: 'Delete a single booking item identified by its uuid, if permitted for the current subject.' operationId: deleteBookingIemByUuid parameters: - - $ref: './auth.yaml#/components/parameters/currentUser' - - $ref: './auth.yaml#/components/parameters/assumedRoles' + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: bookingItemUuid in: path required: true @@ -76,8 +76,8 @@ delete: "204": description: No Content "401": - $ref: './error-responses.yaml#/components/responses/Unauthorized' + $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": - $ref: './error-responses.yaml#/components/responses/Forbidden' + $ref: 'error-responses.yaml#/components/responses/Forbidden' "404": - $ref: './error-responses.yaml#/components/responses/NotFound' + $ref: 'error-responses.yaml#/components/responses/NotFound' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml index 1e6679b4..e869af21 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml @@ -5,8 +5,8 @@ get: - hs-booking-items operationId: listBookingItemsByDebitorUuid parameters: - - $ref: './auth.yaml#/components/parameters/currentUser' - - $ref: './auth.yaml#/components/parameters/assumedRoles' + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: debitorUuid in: query required: true @@ -22,11 +22,11 @@ get: schema: type: array items: - $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' "401": - $ref: './error-responses.yaml#/components/responses/Unauthorized' + $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": - $ref: './error-responses.yaml#/components/responses/Forbidden' + $ref: 'error-responses.yaml#/components/responses/Forbidden' post: summary: Adds a new booking item. @@ -34,25 +34,25 @@ post: - hs-booking-items operationId: addBookingItem parameters: - - $ref: './auth.yaml#/components/parameters/currentUser' - - $ref: './auth.yaml#/components/parameters/assumedRoles' + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' requestBody: description: A JSON object describing the new booking item. required: true content: application/json: schema: - $ref: '/hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemInsert' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItemInsert' responses: "201": description: Created content: 'application/json': schema: - $ref: './hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' + $ref: 'hs-booking-item-schemas.yaml#/components/schemas/HsBookingItem' "401": - $ref: './error-responses.yaml#/components/responses/Unauthorized' + $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": - $ref: './error-responses.yaml#/components/responses/Forbidden' + $ref: 'error-responses.yaml#/components/responses/Forbidden' "409": - $ref: './error-responses.yaml#/components/responses/Conflict' + $ref: 'error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking.yaml b/src/main/resources/api-definition/hs-booking/hs-booking.yaml index 8da21d09..d6a67058 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking.yaml @@ -11,7 +11,7 @@ paths: # Items /api/hs/booking/items: - $ref: "./hs-booking-items.yaml" + $ref: "hs-booking-items.yaml" /api/hs/booking/items/{bookingItemUuid}: - $ref: "./hs-booking-items-with-uuid.yaml" + $ref: "hs-booking-items-with-uuid.yaml" diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql index 7a688e42..6326ce7f 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql @@ -32,9 +32,9 @@ begin raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; insert into hs_booking_item (uuid, debitoruuid, caption, validity, resources) - values (uuid_generate_v4(), relatedDebitor.uuid, 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "SDD-storage": 512 }'::jsonb), - (uuid_generate_v4(), relatedDebitor.uuid, 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "HDD-storage": 1024 }'::jsonb), - (uuid_generate_v4(), relatedDebitor.uuid, 'some Whatever', daterange('20240401', null, '[]'), '{ "CPUs": 1, "SDD-storage": 512, "HDD-storage": 2048 }'::jsonb); + values (uuid_generate_v4(), relatedDebitor.uuid, 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb), + (uuid_generate_v4(), relatedDebitor.uuid, 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb), + (uuid_generate_v4(), relatedDebitor.uuid, 'some Whatever', daterange('20240401', null, '[]'), '{ "CPU": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 7579e23c..23f61106 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -69,23 +69,36 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" [ { - "caption": "some ManagedServer", - "validFrom": "2022-10-01", - "validTo": null, - "resources": { CPUs: 2, SDD-storage: 512 } - }, - { - "caption": "some CloudServer", - "validFrom": "2023-01-15", - "validTo": "2024-04-14", - "resources": { CPUs: 2, HDD-storage: 1024 } - }, - { - "caption": "some Whatever", - "validFrom": "2024-04-01", - "validTo": null, - "resources": { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 } - } + "caption": "some ManagedServer", + "validFrom": "2022-10-01", + "validTo": null, + "resources": { + "CPU": 2, + "SDD": 512, + "extra": 42 + } + }, + { + "caption": "some CloudServer", + "validFrom": "2023-01-15", + "validTo": "2024-04-14", + "resources": { + "CPU": 2, + "HDD": 1024, + "extra": 42 + } + }, + { + "caption": "some Whatever", + "validFrom": "2024-04-01", + "validTo": null, + "resources": { + "CPU": 1, + "HDD": 2048, + "SDD": 512, + "extra": 42 + } + } ] """)); // @formatter:on @@ -109,7 +122,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { "debitorUuid": "%s", "caption": "some new booking", - "resources": { "something": 12 }, + "resources": { "CPU": 12, "extra": 42 }, "validFrom": "2022-10-13" } """.formatted(givenDebitor.getUuid())) @@ -124,7 +137,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some new booking", "validFrom": "2022-10-13", "validTo": null, - "resources": { "something": 12 } + "resources": { "CPU": 12 } } """)) .header("Location", startsWith("http://localhost")) @@ -162,7 +175,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some CloudServer", "validFrom": "2023-01-15", "validTo": "2024-04-14", - "resources": { CPUs: 2, HDD-storage: 1024 } + "resources": { CPU: 2, HDD: 1024 } } """)); // @formatter:on } @@ -207,7 +220,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some CloudServer", "validFrom": "2023-01-15", "validTo": "2024-04-14", - "resources": { CPUs: 2, HDD-storage: 1024 } + "resources": { CPU: 2, HDD: 1024 } } """)); // @formatter:on } @@ -219,7 +232,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() { - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); RestAssured // @formatter:off .given() @@ -230,9 +243,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2020-06-05", "validTo": "2022-12-31", "resources": { - "CPUs": "4", - "HDD_storage": null, - "SSD-storage": "4096" + "CPU": "4", + "HDD": null, + "SSD": "4096" } } """) @@ -248,8 +261,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2020-06-05", "validTo": "2022-12-31", "resources": { - "CPUs": "2", - "SSD-storage": "2048" + "CPU": "4", + "SSD": "4096", + "something": 1 } } """)); // @formatter:on @@ -269,9 +283,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canPatchJustValidToOfArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); - final var location = RestAssured // @formatter:off + RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) @@ -292,7 +306,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2022-11-01", "validTo": "2022-12-31", "resources": { - "CPUs": 5 + "something": 1 } } """)); // @formatter:on @@ -310,7 +324,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canNotPatchReferenceOfArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); final var location = RestAssured // @formatter:off .given() @@ -344,7 +358,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canDeleteArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); RestAssured // @formatter:off .given() @@ -362,7 +376,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotDeleteUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); RestAssured // @formatter:off .given() @@ -378,7 +392,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } } - private HsBookingItemEntity givenSomeTemporaryBookingItemForDebitorNumber(final int debitorNumber) { + private HsBookingItemEntity givenSomeTemporaryBookingItemForDebitorNumber(final int debitorNumber, + final Map.Entry resources) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); @@ -386,7 +401,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .uuid(UUID.randomUUID()) .debitor(givenDebitor) .caption("some test-booking") - .resources(Map.ofEntries(entry("something", 1))) + .resources(Map.ofEntries(resources)) .validity(Range.closedOpen( LocalDate.parse("2022-11-01"), LocalDate.parse("2023-03-31"))) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 5163305f..dd3a59ee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.hs.booking.item; import io.hypersistence.utils.hibernate.type.range.Range; -import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; @@ -13,11 +12,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import jakarta.persistence.EntityManager; import java.time.LocalDate; +import java.util.Map; import java.util.UUID; import java.util.stream.Stream; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntityPatcher.objectToMap; import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.mapper.PatchMap.entry; +import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -35,15 +37,22 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< private static final LocalDate PATCHED_VALID_FROM = LocalDate.parse("2022-10-30"); private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); - private static final ArbitraryBookingResourcesJsonResource INITIAL_RESOURCES = new ArbitraryBookingResourcesJsonResource() { - Integer cpus = 1; - Integer hddStorage = 1024; - }; - private static final ArbitraryBookingResourcesJsonResource PATCHED_RESOURCES = new ArbitraryBookingResourcesJsonResource() { - Integer cpus = 2; - Integer sddStorage = 256; - Integer hddStorage = null; - }; + private static final Map INITIAL_RESOURCES = patchMap( + entry("CPU", 1), + entry("HDD", 1024), + entry("MEM", 64) + ); + private static final Map PATCH_RESOURCES = patchMap( + entry("CPU", 2), + entry("HDD", null), + entry("SDD", 256) + ); + private static final Map PATCHED_RESOURCES = patchMap( + entry("CPU", 2), + entry("SDD", 256), + entry("MEM", 64) + ); + private static final String INITIAL_CAPTION = "initial caption"; private static final String PATCHED_CAPTION = "patched caption"; @@ -90,9 +99,9 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< new SimpleProperty<>( "resources", HsBookingItemPatchResource::setResources, - PATCHED_RESOURCES, + PATCH_RESOURCES, HsBookingItemEntity::putResources, - objectToMap(PATCHED_RESOURCES)) + PATCHED_RESOURCES) .notNullable(), new JsonNullableProperty<>( "validfrom", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index e1558f95..ad112896 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -157,9 +157,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212, [2022-10-01,), some ManagedServer, { CPUs: 2, SDD-storage: 512 })", - "HsBookingItemEntity(D-1000212, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD-storage: 1024 })", - "HsBookingItemEntity(D-1000212, [2024-04-01,), some Whatever, { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 })"); + "HsBookingItemEntity(D-1000212, [2022-10-01,), some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })", + "HsBookingItemEntity(D-1000212, [2023-01-15,2024-04-15), some CloudServer, { CPU: 2, HDD: 1024, extra: 42 })", + "HsBookingItemEntity(D-1000212, [2024-04-01,), some Whatever, { CPU: 1, HDD: 2048, SDD: 512, extra: 42 })"); } @Test @@ -174,9 +174,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, [2022-10-01,), some ManagedServer, { CPUs: 2, SDD-storage: 512 })", - "HsBookingItemEntity(D-1000111, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD-storage: 1024 })", - "HsBookingItemEntity(D-1000111, [2024-04-01,), some Whatever, { CPUs: 1, HDD-storage: 2048, SDD-storage: 512 })"); + "HsBookingItemEntity(D-1000111, [2022-10-01,), some ManagedServer, { CPU: 2, SDD: 512, extra: 42 })", + "HsBookingItemEntity(D-1000111, [2023-01-15,2024-04-15), some CloudServer, { CPU: 2, HDD: 1024, extra: 42 })", + "HsBookingItemEntity(D-1000111, [2024-04-01,), some Whatever, { CPU: 1, HDD: 2048, SDD: 512, extra: 42 })"); } } -- 2.39.5 From 1d0121f79d9454b264c30be1ca2bb7ab36dbcc7c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 08:57:40 +0200 Subject: [PATCH 15/21] cleanup --- build.gradle | 1 - .../item/HsBookingItemEntityPatcher.java | 35 +++---------------- .../HsBookingItemEntityPatcherUnitTest.java | 4 +-- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index 4867a81c..254949e5 100644 --- a/build.gradle +++ b/build.gradle @@ -157,7 +157,6 @@ openapiProcessor { targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true - // generateAliasAsModel true } } sourceSets.main.java.srcDir 'build/generated/sources/openapi' diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 1905967b..8aa0e198 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -3,14 +3,10 @@ package net.hostsharing.hsadminng.hs.booking.item; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.OptionalFromJson; -import org.apache.commons.lang3.tuple.ImmutablePair; -import java.util.HashMap; import java.util.Map; import java.util.Optional; -import static java.util.Arrays.stream; - public class HsBookingItemEntityPatcher implements EntityPatcher { private final HsBookingItemEntity entity; @@ -24,40 +20,19 @@ public class HsBookingItemEntityPatcher implements EntityPatcher entity.getResources().patch(objectToMap(resource.getResources()))); + .ifPresent(r -> entity.getResources().patch(castToKeyValueMap(resource.getResources()))); OptionalFromJson.of(resource.getValidFrom()) .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) .ifPresent(entity::setValidTo); } - static Map objectToMap(final Object obj) { + @SuppressWarnings("unchecked") + static Map castToKeyValueMap(final Object obj) { if (obj instanceof Map) { - return toKeyValueMap(obj); + return (Map) obj; } - return stream(obj.getClass().getDeclaredFields()) - .map(field -> { - try { - field.setAccessible(true); - return new ImmutablePair<>(field.getName(), field.get(obj)); - } catch (final IllegalAccessException exc) { - throw new RuntimeException(exc); - } - }) - .reduce( - new HashMap<>(), - (map, pair) -> { - map.put(pair.getKey(), pair.getValue()); - return map; - }, - (map1, map2) -> { - map1.putAll(map2); - return map1; - }); + throw new ClassCastException("Map expected, but got: " + obj); } - @SuppressWarnings("unchecked") - private static Map toKeyValueMap(final Object obj) { - return (Map) obj; - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index dd3a59ee..9234a3ec 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -16,7 +16,7 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Stream; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntityPatcher.objectToMap; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; @@ -72,7 +72,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< final var entity = new HsBookingItemEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); entity.setDebitor(TEST_DEBITOR); - entity.getResources().putAll(objectToMap(INITIAL_RESOURCES)); + entity.getResources().putAll(castToKeyValueMap(INITIAL_RESOURCES)); entity.setCaption(INITIAL_CAPTION); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); return entity; -- 2.39.5 From 33cffed85b926231b3e15040cf64c11630910d9f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 09:14:08 +0200 Subject: [PATCH 16/21] fix Location after add booking item --- .../hsadminng/hs/booking/item/HsBookingItemController.java | 2 +- .../booking/item/HsBookingItemControllerAcceptanceTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 06926266..57ffefcd 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -60,7 +60,7 @@ public class HsBookingItemController implements HsBookingItemsApi { final var uri = MvcUriComponentsBuilder.fromController(getClass()) - .path("/api/hs/office/bookingItems/{id}") + .path("/api/hs/booking/items/{id}") .buildAndExpand(saved.getUuid()) .toUri(); final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 23f61106..fa4ce211 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -23,7 +23,7 @@ import java.util.UUID; import static java.util.Map.entry; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.matchesRegex; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -140,7 +140,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "resources": { "CPU": 12 } } """)) - .header("Location", startsWith("http://localhost")) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) .extract().header("Location"); // @formatter:on // finally, the new bookingItem can be accessed under the generated UUID -- 2.39.5 From 03e6c60c72d34b5fdde5df07fd94d39b9f6d2b3a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 09:20:22 +0200 Subject: [PATCH 17/21] refactor castToKeyValueMap --- .../hs/booking/item/HsBookingItemController.java | 4 ++-- .../hs/booking/item/HsBookingItemEntityPatcher.java | 12 ++---------- .../hostsharing/hsadminng/mapper/EntityPatcher.java | 11 +++++++++++ .../item/HsBookingItemEntityPatcherUnitTest.java | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 57ffefcd..16e79118 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -13,10 +13,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; +import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController @@ -126,6 +126,6 @@ public class HsBookingItemController implements HsBookingItemsApi { @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); - entity.putResources((Map) resource.getResources()); + entity.putResources(castToKeyValueMap(resource.getResources())); }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 8aa0e198..e6a6a101 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -4,9 +4,10 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItem import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.OptionalFromJson; -import java.util.Map; import java.util.Optional; +import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; + public class HsBookingItemEntityPatcher implements EntityPatcher { private final HsBookingItemEntity entity; @@ -26,13 +27,4 @@ public class HsBookingItemEntityPatcher implements EntityPatcher castToKeyValueMap(final Object obj) { - if (obj instanceof Map) { - return (Map) obj; - } - throw new ClassCastException("Map expected, but got: " + obj); - } - } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java index 23eb9753..82e18976 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java @@ -1,6 +1,17 @@ package net.hostsharing.hsadminng.mapper; +import java.util.Map; + public interface EntityPatcher { void apply(R resource); + + @SuppressWarnings("unchecked") + public static Map castToKeyValueMap(final Object obj) { + if (obj instanceof Map) { + return (Map) obj; + } + throw new ClassCastException("Map expected, but got: " + obj); + } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 9234a3ec..34d5a213 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -16,8 +16,8 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Stream; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; -- 2.39.5 From c8a3070facb6e459d05045f9a8287448951e3f94 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 09:33:52 +0200 Subject: [PATCH 18/21] castToKeyValueMap -> KeyValueMap.from --- .../hs/booking/item/HsBookingItemController.java | 4 ++-- .../booking/item/HsBookingItemEntityPatcher.java | 4 ++-- .../hsadminng/mapper/EntityPatcher.java | 11 ----------- .../hostsharing/hsadminng/mapper/KeyValueMap.java | 14 ++++++++++++++ .../item/HsBookingItemEntityPatcherUnitTest.java | 4 ++-- 5 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index 16e79118..bd05ad66 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -16,7 +17,6 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController @@ -126,6 +126,6 @@ public class HsBookingItemController implements HsBookingItemsApi { @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); - entity.putResources(castToKeyValueMap(resource.getResources())); + entity.putResources(KeyValueMap.from(resource.getResources())); }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index e6a6a101..2131bef5 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -2,11 +2,11 @@ package net.hostsharing.hsadminng.hs.booking.item; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; +import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.OptionalFromJson; import java.util.Optional; -import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; public class HsBookingItemEntityPatcher implements EntityPatcher { @@ -21,7 +21,7 @@ public class HsBookingItemEntityPatcher implements EntityPatcher entity.getResources().patch(castToKeyValueMap(resource.getResources()))); + .ifPresent(r -> entity.getResources().patch(KeyValueMap.from(resource.getResources()))); OptionalFromJson.of(resource.getValidFrom()) .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java index 82e18976..23eb9753 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/EntityPatcher.java @@ -1,17 +1,6 @@ package net.hostsharing.hsadminng.mapper; -import java.util.Map; - public interface EntityPatcher { void apply(R resource); - - @SuppressWarnings("unchecked") - public static Map castToKeyValueMap(final Object obj) { - if (obj instanceof Map) { - return (Map) obj; - } - throw new ClassCastException("Map expected, but got: " + obj); - } - } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java new file mode 100644 index 00000000..5a8cff2f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/KeyValueMap.java @@ -0,0 +1,14 @@ +package net.hostsharing.hsadminng.mapper; + +import java.util.Map; + +public class KeyValueMap { + + @SuppressWarnings("unchecked") + public static Map from(final Object obj) { + if (obj instanceof Map) { + return (Map) obj; + } + throw new ClassCastException("Map expected, but got: " + obj); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 34d5a213..7e7f4308 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -17,7 +18,6 @@ import java.util.UUID; import java.util.stream.Stream; import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; -import static net.hostsharing.hsadminng.mapper.EntityPatcher.castToKeyValueMap; import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @@ -72,7 +72,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< final var entity = new HsBookingItemEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); entity.setDebitor(TEST_DEBITOR); - entity.getResources().putAll(castToKeyValueMap(INITIAL_RESOURCES)); + entity.getResources().putAll(KeyValueMap.from(INITIAL_RESOURCES)); entity.setCaption(INITIAL_CAPTION); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); return entity; -- 2.39.5 From 93cf73b78e1d36beab55716e84f2d2fb8428995f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 09:39:35 +0200 Subject: [PATCH 19/21] validFrom not patchable anymore --- .../hs/booking/item/HsBookingItemEntityPatcher.java | 2 -- .../api-definition/hs-booking/hs-booking-item-schemas.yaml | 4 ---- .../booking/item/HsBookingItemControllerAcceptanceTest.java | 2 +- .../hs/booking/item/HsBookingItemEntityPatcherUnitTest.java | 6 ------ 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 2131bef5..24f2f41c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -22,8 +22,6 @@ public class HsBookingItemEntityPatcher implements EntityPatcher entity.getResources().patch(KeyValueMap.from(resource.getResources()))); - OptionalFromJson.of(resource.getValidFrom()) - .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) .ifPresent(entity::setValidTo); } diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 7424a46e..bf64d994 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -32,10 +32,6 @@ components: caption: type: string nullable: true - validFrom: - type: string - format: date - nullable: true validTo: type: string format: date diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index fa4ce211..55bdbb2c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -258,7 +258,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "caption": "some test-booking", - "validFrom": "2020-06-05", + "validFrom": "2022-11-01", "validTo": "2022-12-31", "resources": { "CPU": "4", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index 7e7f4308..b7ff8ab4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -34,7 +34,6 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< private static final UUID INITIAL_BOOKING_ITEM_UUID = UUID.randomUUID(); private static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-04-15"); - private static final LocalDate PATCHED_VALID_FROM = LocalDate.parse("2022-10-30"); private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); private static final Map INITIAL_RESOURCES = patchMap( @@ -103,11 +102,6 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< HsBookingItemEntity::putResources, PATCHED_RESOURCES) .notNullable(), - new JsonNullableProperty<>( - "validfrom", - HsBookingItemPatchResource::setValidFrom, - PATCHED_VALID_FROM, - HsBookingItemEntity::setValidFrom), new JsonNullableProperty<>( "validto", HsBookingItemPatchResource::setValidTo, -- 2.39.5 From 289c997628e16dc195f4c83bdc88ea1cb9d1fa02 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 09:54:47 +0200 Subject: [PATCH 20/21] cleanup --- .../resources/api-definition/hs-booking/api-mappings.yaml | 1 - .../api-definition/hs-booking/hs-booking-item-schemas.yaml | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/main/resources/api-definition/hs-booking/api-mappings.yaml b/src/main/resources/api-definition/hs-booking/api-mappings.yaml index bce81426..e16861f0 100644 --- a/src/main/resources/api-definition/hs-booking/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-booking/api-mappings.yaml @@ -11,7 +11,6 @@ map: types: - type: array => java.util.List - type: string:uuid => java.util.UUID - - type: string:format => java.lang.String paths: /api/hs/booking/items/{bookingItemUuid}: diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index bf64d994..a9f946b7 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -68,12 +68,6 @@ components: - resources additionalProperties: false - BookingResourcesDoesNotWork: - type: object - x-javaType: 'java.util.Map' - additionalProperties: - type: object - BookingResources: anyOf: - $ref: '#/components/schemas/ManagedServerBookingResources' -- 2.39.5 From 590f32ae4797f6ee4068c0d6b995cdd2ed5723ed Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 16 Apr 2024 11:17:47 +0200 Subject: [PATCH 21/21] amendmends after code-review --- .../hs-booking/hs-booking-item-schemas.yaml | 1 - ...HsBookingItemControllerAcceptanceTest.java | 72 ------------------- ...sBookingItemRepositoryIntegrationTest.java | 14 ++-- 3 files changed, 7 insertions(+), 80 deletions(-) diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index a9f946b7..06f8b921 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -21,7 +21,6 @@ components: $ref: '#/components/schemas/BookingResources' required: - uuid - - debitor - validFrom - validTo - resources diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 55bdbb2c..cbd56570 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -278,78 +278,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup return true; }); } - - @Test - void globalAdmin_canPatchJustValidToOfArbitraryBookingItem() { - - context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); - - RestAssured // @formatter:off - .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "validTo": "2022-12-31" - } - """) - .port(port) - .when() - .patch("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) - .then().log().all().assertThat() - .statusCode(200) - .contentType(ContentType.JSON) - .body("", lenientlyEquals(""" - { - "caption": "some test-booking", - "validFrom": "2022-11-01", - "validTo": "2022-12-31", - "resources": { - "something": 1 - } - } - """)); // @formatter:on - - // finally, the bookingItem is actually updated - assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() - .matches(mandate -> { - assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); - assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)"); - return true; - }); - } - - @Test - void globalAdmin_canNotPatchReferenceOfArbitraryBookingItem() { - - context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeTemporaryBookingItemForDebitorNumber(1000111, entry("something", 1)); - - final var location = RestAssured // @formatter:off - .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "reference": "temp ref CAT new" - } - """) - .port(port) - .when() - .patch("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) - .then().assertThat() - // TODO.impl: I'd prefer a 400, - // but OpenApi Spring Code Gen does not convert additonalProperties=false into a validation - .statusCode(200); // @formatter:on - - // finally, the bookingItem is actually updated - assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() - .matches(mandate -> { - assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-03-31)"); - return true; - }); - } } @Nested diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index ad112896..9a5eaf00 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -121,17 +121,17 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup "{ grant perm:hs_booking_item#D-1000111:some new booking item:DELETE to role:global#global:ADMIN by system and assume }", // owner + "{ grant perm:hs_booking_item#D-1000111:some new booking item:UPDATE to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", + "{ grant role:hs_booking_item#D-1000111:some new booking item:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", // admin + "{ grant role:hs_booking_item#D-1000111:some new booking item:ADMIN to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", // tenant - - "{ grant perm:hs_booking_item#D-1000111:some new booking item:SELECT to role:hs_booking_item#D-1000111:some new booking item:TENANT by system and assume }", - "{ grant perm:hs_booking_item#D-1000111:some new booking item:UPDATE to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", - "{ grant role:hs_booking_item#D-1000111:some new booking item:ADMIN to role:hs_booking_item#D-1000111:some new booking item:OWNER by system and assume }", - "{ grant role:hs_booking_item#D-1000111:some new booking item:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", "{ grant role:hs_booking_item#D-1000111:some new booking item:TENANT to role:hs_booking_item#D-1000111:some new booking item:ADMIN by system and assume }", + "{ grant perm:hs_booking_item#D-1000111:some new booking item:SELECT to role:hs_booking_item#D-1000111:some new booking item:TENANT by system and assume }", "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_item#D-1000111:some new booking item:TENANT by system and assume }", + null)); } @@ -299,8 +299,8 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then assertThat(customerLogEntries).map(Arrays::toString).contains( "[creating booking-item test-data 1000111, hs_booking_item, INSERT]", - "[creating booking-item test-data 1000111, hs_booking_item, INSERT]", - "[creating booking-item test-data 1000111, hs_booking_item, INSERT]"); + "[creating booking-item test-data 1000212, hs_booking_item, INSERT]", + "[creating booking-item test-data 1000313, hs_booking_item, INSERT]"); } private HsBookingItemEntity givenSomeTemporaryBookingItem(final int debitorNumber) { -- 2.39.5