From 04d9b433016f2698776fc100d3337356ef0addd7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 20 Jun 2024 10:44:28 +0200 Subject: [PATCH] BookingItem validity start date today (#62) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/62 Reviewed-by: Marc Sandlus --- .../booking/item/HsBookingItemController.java | 4 +- .../hs/booking/item/HsBookingItemEntity.java | 4 +- .../hs-booking/hs-booking-item-schemas.yaml | 4 - ...HsBookingItemControllerAcceptanceTest.java | 21 ++- .../item/HsBookingItemControllerRestTest.java | 171 ++++++++++++++++++ .../item/HsBookingItemEntityUnitTest.java | 24 +++ ...HostingAssetRepositoryIntegrationTest.java | 2 - 7 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.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 1343378c..36c16a32 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 @@ -16,6 +16,7 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -130,9 +131,8 @@ public class HsBookingItemController implements HsBookingItemsApi { } }; - @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); + entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo())); entity.putResources(KeyValueMap.from(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 0856f866..90774110 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 @@ -17,8 +17,6 @@ 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.NotFound; -import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.Type; import jakarta.persistence.CascadeType; @@ -101,7 +99,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Builder.Default @Type(PostgreSQLRangeType.class) @Column(name = "validity", columnDefinition = "daterange") - private Range validity = Range.emptyRange(LocalDate.class); + private Range validity = Range.closedInfinite(LocalDate.now()); @Column(name = "caption") private String caption; 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 aa7ab925..b18c7356 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 @@ -62,10 +62,6 @@ components: minLength: 3 maxLength: 80 nullable: false - validFrom: - type: string - format: date - nullable: false 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 bf8b4ba9..5edc23af 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 @@ -144,13 +144,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { - "projectUuid": "%s", + "projectUuid": "{projectUuid}", "type": "MANAGED_SERVER", "caption": "some new booking", - "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }, - "validFrom": "2022-10-13" + "validTo": "{validTo}", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } } - """.formatted(givenProject.getUuid())) + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{validTo}", LocalDate.now().plusMonths(1).toString()) + ) .port(port) .when() .post("http://localhost/api/hs/booking/items") @@ -161,11 +164,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { "type": "MANAGED_SERVER", "caption": "some new booking", - "validFrom": "2022-10-13", - "validTo": null, + "validFrom": "{today}", + "validTo": "{todayPlus1Month}", "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } } - """)) + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + ) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) .extract().header("Location"); // @formatter:on @@ -236,7 +242,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test @Order(3) - // TODO.impl: For unknown reason this test fails in about 50%, not finding the uuid (404). void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = bookingItemRepo.findByCaption("separate ManagedServer").stream() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java new file mode 100644 index 00000000..0fb0f6f0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java @@ -0,0 +1,171 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.SynchronizationType; +import java.time.LocalDate; +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.hamcrest.Matchers.matchesRegex; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HsBookingItemController.class) +@Import(Mapper.class) +@RunWith(SpringRunner.class) +class HsBookingItemControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + Context contextMock; + + @Mock + EntityManager em; + + @MockBean + EntityManagerFactory emf; + + @MockBean + HsBookingProjectRepository bookingProjectRepo; + + @MockBean + HsBookingItemRepository bookingItemRepo; + + @BeforeEach + void init() { + when(emf.createEntityManager()).thenReturn(em); + when(emf.createEntityManager(any(Map.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); + } + + @Nested + class AddBookingItem { + + @Test + void globalAdmin_canAddValidBookingItem() throws Exception { + + final var givenProjectUuid = UUID.randomUUID(); + + // given + when(em.find(HsBookingProjectEntity.class, givenProjectUuid)).thenAnswer(invocation -> + HsBookingProjectEntity.builder() + .uuid(invocation.getArgument(1)) + .build() + ); + when(bookingItemRepo.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post("/api/hs/booking/items") + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validTo": "{validTo}", + "garbage": "should not be accepted", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{projectUuid}", givenProjectUuid.toString()) + .replace("{validTo}", LocalDate.now().plusMonths(1).toString()) + ) + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", lenientlyEquals(""" + { + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{today}", + "validTo": "{todayPlus1Month}", + "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + )) + .andExpect(header().string("Location", matchesRegex("http://localhost/api/hs/booking/items/[^/]*"))); + } + + @Test + void globalAdmin_canNotAddInvalidBookingItem() throws Exception { + + final var givenProjectUuid = UUID.randomUUID(); + + // given + when(em.find(HsBookingProjectEntity.class, givenProjectUuid)).thenAnswer(invocation -> + HsBookingProjectEntity.builder() + .uuid(invocation.getArgument(1)) + .build() + ); + when(bookingItemRepo.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post("/api/hs/booking/items") + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{validFrom}", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{projectUuid}", givenProjectUuid.toString()) + .replace("{validFrom}", LocalDate.now().plusMonths(1).toString()) + ) + .accept(MediaType.APPLICATION_JSON)) + + // then + // TODO.test: MockMvc does not seem to validate additionalProperties=false + // .andExpect(status().is4xxClientError()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", lenientlyEquals(""" + { + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{today}", + "validTo": null, + "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + )) + .andExpect(header().string("Location", matchesRegex("http://localhost/api/hs/booking/items/[^/]*"))); + } + } +} 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 1b95dc8a..903d5385 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 @@ -1,8 +1,12 @@ package net.hostsharing.hsadminng.hs.booking.item; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.time.LocalDate; +import java.time.Month; import java.util.Map; import static java.util.Map.entry; @@ -14,6 +18,8 @@ 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"); + private MockedStatic localDateMockedStatic = Mockito.mockStatic(LocalDate.class, Mockito.CALLS_REAL_METHODS); + final HsBookingItemEntity givenBookingItem = HsBookingItemEntity.builder() .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) @@ -25,6 +31,24 @@ class HsBookingItemEntityUnitTest { .validity(toPostgresDateRange(GIVEN_VALID_FROM, GIVEN_VALID_TO)) .build(); + @AfterEach + void tearDown() { + localDateMockedStatic.close(); + } + + @Test + void validityStartsToday() { + // given + final var fakedToday = LocalDate.of(2024, Month.MAY, 1); + localDateMockedStatic.when(LocalDate::now).thenReturn(fakedToday); + + // when + final var newBookingItem = HsBookingItemEntity.builder().build(); + + // then + assertThat(newBookingItem.getValidity().toString()).isEqualTo("Range{lower=2024-05-01, upper=null, mask=82, clazz=class java.time.LocalDate}"); + } + @Test void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java index 480f9416..f4abe06c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java @@ -24,8 +24,6 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;