introduce-booking-module #41

Merged
hsh-michaelhoennig merged 21 commits from introduce-booking-module into master 2024-04-16 11:21:35 +02:00
13 changed files with 118 additions and 87 deletions
Showing only changes of commit 75c759bed3 - Show all commits

View File

@ -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<HsBookingItemInsertResource, HsBookingItemEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {

View File

@ -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,

View File

@ -74,11 +74,11 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
public ResponseEntity<HsOfficeBankAccountResource> 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();
}

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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;
$$;

View File

@ -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
);

View File

@ -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,7 +64,7 @@ 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")
@ -70,19 +72,22 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("", lenientlyEquals("""
[
{
"debitor": { "debitorNumber": 1000111 },
"caption": "some ManagedServer",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
"validTo": null,
"resources": null
},
{
"debitor": { "debitorNumber": 1000212 },
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
"caption": "some CloudServer",
"validFrom": "2023-01-15",
"validTo": "2024-04-14",
"resources": null
},
{
"debitor": { "debitorNumber": 1000313 },
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
"caption": "some Whatever",
"validFrom": "2024-04-01",
"validTo": null,
"resources": null
}
]
"""));
@ -97,7 +102,7 @@ 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()
@ -106,20 +111,25 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
.body("""
{
"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"))
@ -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,14 +158,15 @@ 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,15 +241,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": "2020-06-05",
"validTo": "2022-12-31",
"resources": null
}
""")); // @formatter:on
@ -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();

View File

@ -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})");
}
}

View File

@ -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 }",