BookingItem validity start date today (#62)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #62
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-06-20 10:44:28 +02:00
parent 62867a4cac
commit 04d9b43301
7 changed files with 211 additions and 19 deletions

View File

@ -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<HsBookingItemInsertResource, HsBookingItemEntity> 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()));
};
}

View File

@ -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<LocalDate> validity = Range.emptyRange(LocalDate.class);
private Range<LocalDate> validity = Range.closedInfinite(LocalDate.now());
@Column(name = "caption")
private String caption;

View File

@ -62,10 +62,6 @@ components:
minLength: 3
maxLength: 80
nullable: false
validFrom:
type: string
format: date
nullable: false
validTo:
type: string
format: date

View File

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

View File

@ -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/[^/]*")));
}
}
}

View File

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

View File

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