From 9bb6563e39138ea1f72bd0c72a86cf34db1d1572 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Fri, 5 Dec 2025 13:58:12 +0100 Subject: [PATCH 01/30] first integration teset --- srv/pom.xml | 4 + .../TravelServiceIntegrationTest.java | 144 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java diff --git a/srv/pom.xml b/srv/pom.xml index ee923f0..6bf7c1d 100644 --- a/srv/pom.xml +++ b/srv/pom.xml @@ -83,6 +83,10 @@ spring-boot-starter-test test + + org.springframework.security + spring-security-test + diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java new file mode 100644 index 0000000..bea7c38 --- /dev/null +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -0,0 +1,144 @@ +package sap.capire.xtravels; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.reflect.CdsModel; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the CAP Travel Service OData endpoints. + * This class provides comprehensive testing of OData operations including: + * - Basic CRUD operations on Travels entity + * - OData query options ($filter, $select, $orderby, etc.) + * - Custom actions (acceptTravel, rejectTravel, deductDiscount) + * - Read-only entities (Flights, Supplements, Currencies) + * - Error handling scenarios + */ +@SpringBootTest +@AutoConfigureWebMvc +class TravelServiceIntegrationTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private CdsModel cdsModel; + + @Autowired + private ObjectMapper objectMapper; + + private static final String ODATA_BASE_URL = "/odata/v4/travel"; + + @Test + void contextLoads() { + assertNotNull(webApplicationContext); + } + + @Test + @WithMockUser("admin") + void shouldGetMetadataSuccessfully() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + mockMvc.perform(get(ODATA_BASE_URL + "/$metadata")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/xml")); + } + + @Test + @WithMockUser("admin") + void shouldGetTravelsCollectionSuccessfully() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + mockMvc.perform(get(ODATA_BASE_URL + "/Travels")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + } + + @Test + @WithMockUser("admin") + void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + Map travelData = new HashMap<>(); + travelData.put("Description", "Integration Test Travel"); + travelData.put("BeginDate", LocalDate.now().plusDays(10).toString()); + travelData.put("EndDate", LocalDate.now().plusDays(17).toString()); + travelData.put("BookingFee", 200.0); + travelData.put("Currency_code", "USD"); + + // Create travel + String response = mockMvc.perform(post(ODATA_BASE_URL + "/Travels") + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify the created travel can be retrieved + Map createdTravel = objectMapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + assertNotNull(travelId); + + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=false)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + } + + @Test + @WithMockUser("admin") + void shouldSupportODataFilterQuery() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + } + + @Test + @WithMockUser("admin") + void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + // Test Flights entity + mockMvc.perform(get(ODATA_BASE_URL + "/Flights")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + + // Test Supplements entity + mockMvc.perform(get(ODATA_BASE_URL + "/Supplements")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + + // Test Currencies entity + mockMvc.perform(get(ODATA_BASE_URL + "/Currencies")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + } + + @Test + @WithMockUser("admin") + void shouldReturn404ForNonExistentEntity() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID=9999999,IsActiveEntity=false)")) + .andExpect(status().isNotFound()); + } +} From c6e99f959ae56e16af6581fc0f152cf2800174e2 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Fri, 5 Dec 2025 15:09:10 +0100 Subject: [PATCH 02/30] extending tests --- .../TravelServiceIntegrationTest.java | 81 ++++- .../xtravels/TravelServiceODataTest.java | 323 ++++++++++++++++++ 2 files changed, 391 insertions(+), 13 deletions(-) create mode 100644 srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index bea7c38..c006540 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.reflect.CdsModel; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; @@ -16,10 +16,16 @@ import java.util.HashMap; import java.util.Map; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -35,16 +41,26 @@ @AutoConfigureWebMvc class TravelServiceIntegrationTest { - @Autowired - private WebApplicationContext webApplicationContext; + private static final String ODATA_BASE_URL = "/odata/v4/travel"; + private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels"; + private static final String FLIGHTS_ENDPOINT = ODATA_BASE_URL + "/Flights"; + private static final String SUPPLEMENTS_ENDPOINT = ODATA_BASE_URL + "/Supplements"; + private static final String CURRENCIES_ENDPOINT = ODATA_BASE_URL + "/Currencies"; @Autowired - private CdsModel cdsModel; + private WebApplicationContext webApplicationContext; @Autowired private ObjectMapper objectMapper; - private static final String ODATA_BASE_URL = "/odata/v4/travel"; + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .build(); + } @Test void contextLoads() { @@ -54,21 +70,23 @@ void contextLoads() { @Test @WithMockUser("admin") void shouldGetMetadataSuccessfully() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - + mockMvc.perform(get(ODATA_BASE_URL + "/$metadata")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/xml")); } + // ========== Travels Entity Tests ========== + @Test @WithMockUser("admin") - void shouldGetTravelsCollectionSuccessfully() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - - mockMvc.perform(get(ODATA_BASE_URL + "/Travels")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); + void shouldGetAllTravels() throws Exception { + + mockMvc.perform(get(TRAVELS_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.@context", containsString("Travels"))) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); } @Test @@ -141,4 +159,41 @@ void shouldReturn404ForNonExistentEntity() throws Exception { mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID=9999999,IsActiveEntity=false)")) .andExpect(status().isNotFound()); } + + @Test + void shouldReturn400ForInvalidDiscountPercentage() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Try to execute deductDiscount action with invalid percentage (>100) + Map actionParams = new HashMap<>(); + actionParams.put("percent", 150); // Invalid percentage + + mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.deductDiscount") + .contentType("application/json") + .content(objectMapper.writeValueAsString(actionParams))) + .andExpect(status().isBadRequest()); + } + + private Map createTravelData() { + Map travelData = new HashMap<>(); + travelData.put("Description", "Test Travel to Paris"); + travelData.put("BeginDate", LocalDate.now().plusDays(30).toString()); + travelData.put("EndDate", LocalDate.now().plusDays(37).toString()); + travelData.put("BookingFee", 100.0); + travelData.put("Currency_code", "EUR"); + travelData.put("Gender", "male"); + return travelData; + } + } diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java new file mode 100644 index 0000000..72659ff --- /dev/null +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java @@ -0,0 +1,323 @@ +package sap.capire.xtravels; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +@SpringBootTest +@AutoConfigureWebMvc +class TravelServiceODataTest { + /* + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private ObjectMapper objectMapper; + + + + + + // ========== Service Document and Metadata Tests ========== + + + + @Test + void shouldGetTravelById() throws Exception { + // First create a travel to ensure we have data + Map travelData = createTravelData(); + + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Now get the travel by ID + mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.ID", is(travelId))) + .andExpect(jsonPath("$.Description", is(travelData.get("Description")))); + } + + @Test + void shouldCreateTravel() throws Exception { + Map travelData = createTravelData(); + + mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.Description", is(travelData.get("Description")))) + .andExpect(jsonPath("$.BeginDate", is(travelData.get("BeginDate")))) + .andExpect(jsonPath("$.EndDate", is(travelData.get("EndDate")))) + .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) + .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))) + .andExpect(jsonPath("$.ID", notNullValue())); + } + + @Test + void shouldUpdateTravel() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Update the travel + Map updateData = new HashMap<>(); + updateData.put("Description", "Updated Travel Description"); + updateData.put("BookingFee", 150.0); + + mockMvc.perform(patch(TRAVELS_ENDPOINT + "(" + travelId + ")") + .contentType("application/json") + .content(objectMapper.writeValueAsString(updateData))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.Description", is("Updated Travel Description"))) + .andExpect(jsonPath("$.BookingFee", is(150.0))); + } + + @Test + void shouldDeleteTravel() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Delete the travel + mockMvc.perform(delete(TRAVELS_ENDPOINT + "(" + travelId + ")")) + .andExpect(status().isNoContent()); + + // Verify it's deleted + mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) + .andExpect(status().isNotFound()); + } + + // ========== OData Query Options Tests ========== + + @Test + void shouldSupportFilterQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$filter=Currency_code eq 'EUR'")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldSupportSelectQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldSupportOrderByQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldSupportTopAndSkipQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=5&$skip=0")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldSupportCountQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "/$count")) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/plain;charset=UTF-8")) + .andExpect(content().string(matchesRegex("\\d+"))); + } + + // ========== Travel Actions Tests ========== + + @Test + void shouldExecuteAcceptTravelAction() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute acceptTravel action + mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.acceptTravel") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()); + } + + @Test + void shouldExecuteRejectTravelAction() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute rejectTravel action + mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.rejectTravel") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()); + } + + @Test + void shouldExecuteDeductDiscountAction() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute deductDiscount action with 10% discount + Map actionParams = new HashMap<>(); + actionParams.put("percent", 10); + + mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.deductDiscount") + .contentType("application/json") + .content(objectMapper.writeValueAsString(actionParams))) + .andExpect(status().isOk()); + } + + // ========== Read-only Entities Tests ========== + + @Test + void shouldGetAllFlights() throws Exception { + mockMvc.perform(get(FLIGHTS_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.@odata.context", containsString("Flights"))) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldGetAllSupplements() throws Exception { + mockMvc.perform(get(SUPPLEMENTS_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.@odata.context", containsString("Supplements"))) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldGetAllCurrencies() throws Exception { + mockMvc.perform(get(CURRENCIES_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.@odata.context", containsString("Currencies"))) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + void shouldNotAllowPostToReadOnlyFlights() throws Exception { + Map flightData = new HashMap<>(); + flightData.put("PlaneType", "Boeing 747"); + + mockMvc.perform(post(FLIGHTS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(flightData))) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + void shouldNotAllowPutToReadOnlySupplements() throws Exception { + Map supplementData = new HashMap<>(); + supplementData.put("Description", "Premium Meal"); + + mockMvc.perform(put(SUPPLEMENTS_ENDPOINT + "(1)") + .contentType("application/json") + .content(objectMapper.writeValueAsString(supplementData))) + .andExpect(status().isMethodNotAllowed()); + } + + // ========== Error Handling Tests ========== + + @Test + void shouldReturn400ForInvalidTravelData() throws Exception { + Map invalidData = new HashMap<>(); + invalidData.put("BeginDate", "invalid-date"); + + mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(invalidData))) + .andExpect(status().isBadRequest()); + } + + @Test + void shouldReturn404ForNonExistentTravel() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "(99999)")) + .andExpect(status().isNotFound()); + } + + + + // ========== Helper Methods ========== + */ +} From 3af259c2298b50236807646947a1e861b4136f70 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Fri, 5 Dec 2025 16:01:10 +0100 Subject: [PATCH 03/30] more tests --- .../TravelServiceIntegrationTest.java | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index c006540..af7d1fa 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -17,11 +17,11 @@ import java.util.Map; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -89,11 +89,28 @@ void shouldGetAllTravels() throws Exception { .andExpect(jsonPath("$.value", isA(java.util.List.class))); } + @Test + @WithMockUser("admin") + void shouldCreateTravel() throws Exception { + Map travelData = createTravelData(); + + mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Description", is(travelData.get("Description")))) + .andExpect(jsonPath("$.BeginDate", is(travelData.get("BeginDate")))) + .andExpect(jsonPath("$.EndDate", is(travelData.get("EndDate")))) + .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) + .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))) + .andExpect(jsonPath("$.ID", notNullValue())); + } + @Test @WithMockUser("admin") void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - + Map travelData = new HashMap<>(); travelData.put("Description", "Integration Test Travel"); travelData.put("BeginDate", LocalDate.now().plusDays(10).toString()); @@ -123,8 +140,7 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { @Test @WithMockUser("admin") void shouldSupportODataFilterQuery() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - + mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")); @@ -133,8 +149,7 @@ void shouldSupportODataFilterQuery() throws Exception { @Test @WithMockUser("admin") void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - + // Test Flights entity mockMvc.perform(get(ODATA_BASE_URL + "/Flights")) .andExpect(status().isOk()) @@ -154,13 +169,13 @@ void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { @Test @WithMockUser("admin") void shouldReturn404ForNonExistentEntity() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID=9999999,IsActiveEntity=false)")) .andExpect(status().isNotFound()); } @Test + @WithMockUser("admin") void shouldReturn400ForInvalidDiscountPercentage() throws Exception { // First create a travel Map travelData = createTravelData(); @@ -185,14 +200,40 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @WithMockUser("admin") + void shouldDeleteTravel() throws Exception { + // First create a travel + Map travelData = createTravelData(); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Delete the travel + mockMvc.perform(delete(TRAVELS_ENDPOINT + "(" + travelId + ")")) + .andExpect(status().isNoContent()); + + // Verify it's deleted + mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) + .andExpect(status().isNotFound()); + } + + + private Map createTravelData() { Map travelData = new HashMap<>(); travelData.put("Description", "Test Travel to Paris"); travelData.put("BeginDate", LocalDate.now().plusDays(30).toString()); travelData.put("EndDate", LocalDate.now().plusDays(37).toString()); - travelData.put("BookingFee", 100.0); + travelData.put("BookingFee", 100); travelData.put("Currency_code", "EUR"); - travelData.put("Gender", "male"); return travelData; } From 1fa103c66347f4392d2f06b1ee67f0ac8943453f Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 10:17:10 +0100 Subject: [PATCH 04/30] WIP --- .../xtravels/handler/CreationHandler.java | 24 ++++--- .../TravelServiceIntegrationTest.java | 36 ++++++---- .../xtravels/TravelServiceODataTest.java | 67 +------------------ 3 files changed, 37 insertions(+), 90 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java index bc97dce..3173bb3 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java @@ -26,17 +26,21 @@ class CreationHandler implements EventHandler { } // Fill in alternative keys as consecutive numbers for new Travels, Bookings, and Supplements. - // Note: For Travels that can't be done at NEW events, that is when drafts are created, - // but on CREATE only, as multiple users could create new Travels concurrently. - @Before(event = EVENT_CREATE) + // Note: We need to handle both draft creation and final creation to avoid unique constraint violations. + @Before(event = {EVENT_CREATE, EVENT_DRAFT_NEW}) void calculateTravelId(final Travels travel) { - var result = - service.run( - Select.from(TRAVELS) - .where(t -> t.IsActiveEntity().eq(true)) - .columns(t -> t.ID().max().as("maxID"))); - int maxId = (int) result.single().get("maxID"); - travel.setId(++maxId); + // Only set ID if it's not already set (to avoid overwriting existing IDs) + if (travel.getId() == null || travel.getId() == 0) { + synchronized (this) { + var result = + service.run( + Select.from(TRAVELS) + .columns(t -> t.ID().max().as("maxID"))); + var maxIdValue = result.single().get("maxID"); + int maxId = maxIdValue == null ? 0 : (int) maxIdValue; + travel.setId(++maxId); + } + } } // Fill in IDs as sequence numbers -> could be automated by auto-generation diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index af7d1fa..f6ff75c 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -47,14 +47,16 @@ class TravelServiceIntegrationTest { private static final String SUPPLEMENTS_ENDPOINT = ODATA_BASE_URL + "/Supplements"; private static final String CURRENCIES_ENDPOINT = ODATA_BASE_URL + "/Currencies"; + private static int testCounter = 0; + + private MockMvc mockMvc; + @Autowired private WebApplicationContext webApplicationContext; @Autowired private ObjectMapper objectMapper; - private MockMvc mockMvc; - @BeforeEach void setUp() { mockMvc = MockMvcBuilders @@ -92,7 +94,7 @@ void shouldGetAllTravels() throws Exception { @Test @WithMockUser("admin") void shouldCreateTravel() throws Exception { - Map travelData = createTravelData(); + Map travelData = createUniqueTravelData("shouldCreateTravel"); mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") @@ -111,10 +113,7 @@ void shouldCreateTravel() throws Exception { @WithMockUser("admin") void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { - Map travelData = new HashMap<>(); - travelData.put("Description", "Integration Test Travel"); - travelData.put("BeginDate", LocalDate.now().plusDays(10).toString()); - travelData.put("EndDate", LocalDate.now().plusDays(17).toString()); + Map travelData = createUniqueTravelData("shouldCreateAndRetrieveTravelSuccessfully"); travelData.put("BookingFee", 200.0); travelData.put("Currency_code", "USD"); @@ -178,7 +177,7 @@ void shouldReturn404ForNonExistentEntity() throws Exception { @WithMockUser("admin") void shouldReturn400ForInvalidDiscountPercentage() throws Exception { // First create a travel - Map travelData = createTravelData(); + Map travelData = createUniqueTravelData("shouldReturn400ForInvalidDiscountPercentage"); String response = mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") .content(objectMapper.writeValueAsString(travelData))) @@ -204,7 +203,7 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { @WithMockUser("admin") void shouldDeleteTravel() throws Exception { // First create a travel - Map travelData = createTravelData(); + Map travelData = createUniqueTravelData("shouldDeleteTravel"); String response = mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") .content(objectMapper.writeValueAsString(travelData))) @@ -227,14 +226,23 @@ void shouldDeleteTravel() throws Exception { - private Map createTravelData() { + private Map createUniqueTravelData(String testName) { + synchronized (TravelServiceIntegrationTest.class) { + testCounter++; + } + Map travelData = new HashMap<>(); - travelData.put("Description", "Test Travel to Paris"); - travelData.put("BeginDate", LocalDate.now().plusDays(30).toString()); - travelData.put("EndDate", LocalDate.now().plusDays(37).toString()); - travelData.put("BookingFee", 100); + travelData.put("Description", testName + " - Test Travel " + testCounter + " to Paris"); + travelData.put("BeginDate", LocalDate.now().plusDays(30 + testCounter).toString()); + travelData.put("EndDate", LocalDate.now().plusDays(37 + testCounter).toString()); + travelData.put("BookingFee", 100 + testCounter); travelData.put("Currency_code", "EUR"); return travelData; } + @Deprecated + private Map createTravelData() { + return createUniqueTravelData("legacy"); + } + } diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java index 72659ff..f53f11e 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java @@ -1,8 +1,6 @@ package sap.capire.xtravels; -import static org.hamcrest.Matchers.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; @@ -40,46 +38,7 @@ class TravelServiceODataTest { - @Test - void shouldGetTravelById() throws Exception { - // First create a travel to ensure we have data - Map travelData = createTravelData(); - - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - // Now get the travel by ID - mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.ID", is(travelId))) - .andExpect(jsonPath("$.Description", is(travelData.get("Description")))); - } - - @Test - void shouldCreateTravel() throws Exception { - Map travelData = createTravelData(); - - mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.Description", is(travelData.get("Description")))) - .andExpect(jsonPath("$.BeginDate", is(travelData.get("BeginDate")))) - .andExpect(jsonPath("$.EndDate", is(travelData.get("EndDate")))) - .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) - .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))) - .andExpect(jsonPath("$.ID", notNullValue())); - } @Test void shouldUpdateTravel() throws Exception { @@ -109,30 +68,6 @@ void shouldUpdateTravel() throws Exception { .andExpect(jsonPath("$.BookingFee", is(150.0))); } - @Test - void shouldDeleteTravel() throws Exception { - // First create a travel - Map travelData = createTravelData(); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Delete the travel - mockMvc.perform(delete(TRAVELS_ENDPOINT + "(" + travelId + ")")) - .andExpect(status().isNoContent()); - - // Verify it's deleted - mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) - .andExpect(status().isNotFound()); - } - // ========== OData Query Options Tests ========== @Test From 81c049e52b31c1fa1bd0632419c8040862431521 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 14:44:29 +0100 Subject: [PATCH 05/30] revert AI change in production code --- .../xtravels/handler/CreationHandler.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java index 3173bb3..bc97dce 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java @@ -26,21 +26,17 @@ class CreationHandler implements EventHandler { } // Fill in alternative keys as consecutive numbers for new Travels, Bookings, and Supplements. - // Note: We need to handle both draft creation and final creation to avoid unique constraint violations. - @Before(event = {EVENT_CREATE, EVENT_DRAFT_NEW}) + // Note: For Travels that can't be done at NEW events, that is when drafts are created, + // but on CREATE only, as multiple users could create new Travels concurrently. + @Before(event = EVENT_CREATE) void calculateTravelId(final Travels travel) { - // Only set ID if it's not already set (to avoid overwriting existing IDs) - if (travel.getId() == null || travel.getId() == 0) { - synchronized (this) { - var result = - service.run( - Select.from(TRAVELS) - .columns(t -> t.ID().max().as("maxID"))); - var maxIdValue = result.single().get("maxID"); - int maxId = maxIdValue == null ? 0 : (int) maxIdValue; - travel.setId(++maxId); - } - } + var result = + service.run( + Select.from(TRAVELS) + .where(t -> t.IsActiveEntity().eq(true)) + .columns(t -> t.ID().max().as("maxID"))); + int maxId = (int) result.single().get("maxID"); + travel.setId(++maxId); } // Fill in IDs as sequence numbers -> could be automated by auto-generation From 1ec21e95e9bf3af5fee11416895842537a7f3521 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 15:00:28 +0100 Subject: [PATCH 06/30] fix tests --- .../xtravels/TravelServiceIntegrationTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index f6ff75c..85b4fe7 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -47,7 +47,7 @@ class TravelServiceIntegrationTest { private static final String SUPPLEMENTS_ENDPOINT = ODATA_BASE_URL + "/Supplements"; private static final String CURRENCIES_ENDPOINT = ODATA_BASE_URL + "/Currencies"; - private static int testCounter = 0; + private static int testCounter = 0; // for test-data generation private MockMvc mockMvc; @@ -131,7 +131,7 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { Integer travelId = (Integer) createdTravel.get("ID"); assertNotNull(travelId); - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=false)")) + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")); } @@ -216,11 +216,11 @@ void shouldDeleteTravel() throws Exception { Integer travelId = (Integer) createdTravel.get("ID"); // Delete the travel - mockMvc.perform(delete(TRAVELS_ENDPOINT + "(" + travelId + ")")) + mockMvc.perform(delete(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) .andExpect(status().isNoContent()); // Verify it's deleted - mockMvc.perform(get(TRAVELS_ENDPOINT + "(" + travelId + ")")) + mockMvc.perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) .andExpect(status().isNotFound()); } @@ -230,13 +230,16 @@ private Map createUniqueTravelData(String testName) { synchronized (TravelServiceIntegrationTest.class) { testCounter++; } - + Map travelData = new HashMap<>(); + travelData.put("IsActiveEntity", true); travelData.put("Description", testName + " - Test Travel " + testCounter + " to Paris"); travelData.put("BeginDate", LocalDate.now().plusDays(30 + testCounter).toString()); travelData.put("EndDate", LocalDate.now().plusDays(37 + testCounter).toString()); travelData.put("BookingFee", 100 + testCounter); travelData.put("Currency_code", "EUR"); + travelData.put("Agency_ID", "070001"); + travelData.put("Customer_ID", "000001"); return travelData; } From f3403d1499d5a6e781328186bf9726f044bba9da Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 15:05:18 +0100 Subject: [PATCH 07/30] cleanup test --- .../sap/capire/xtravels/TravelServiceIntegrationTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 85b4fe7..c08c332 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -242,10 +242,4 @@ private Map createUniqueTravelData(String testName) { travelData.put("Customer_ID", "000001"); return travelData; } - - @Deprecated - private Map createTravelData() { - return createUniqueTravelData("legacy"); - } - } From 7ecee2410cc22fed9efeb25cbb0e0f0a8ef84e99 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 15:13:04 +0100 Subject: [PATCH 08/30] more simple odata tests --- .../TravelServiceIntegrationTest.java | 36 +++++++++++++++++++ .../xtravels/TravelServiceODataTest.java | 35 ------------------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index c08c332..7250743 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -145,6 +146,41 @@ void shouldSupportODataFilterQuery() throws Exception { .andExpect(content().contentTypeCompatibleWith("application/json")); } + @Test + @WithMockUser("admin") + void shouldSupportSelectQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + @WithMockUser("admin") + void shouldSupportOrderByQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + @WithMockUser("admin") + void shouldSupportTopAndSkipQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=5&$skip=0")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(java.util.List.class))); + } + + @Test + @WithMockUser("admin") + void shouldSupportCountQuery() throws Exception { + mockMvc.perform(get(TRAVELS_ENDPOINT + "/$count")) + .andExpect(status().isOk()) + .andExpect(content().string(matchesRegex("\\d+"))); + } + @Test @WithMockUser("admin") void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java index f53f11e..a38a749 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java @@ -70,45 +70,10 @@ void shouldUpdateTravel() throws Exception { // ========== OData Query Options Tests ========== - @Test - void shouldSupportFilterQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$filter=Currency_code eq 'EUR'")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - @Test - void shouldSupportSelectQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - @Test - void shouldSupportOrderByQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - @Test - void shouldSupportTopAndSkipQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=5&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - @Test - void shouldSupportCountQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "/$count")) - .andExpect(status().isOk()) - .andExpect(content().contentType("text/plain;charset=UTF-8")) - .andExpect(content().string(matchesRegex("\\d+"))); - } // ========== Travel Actions Tests ========== From 34cad1f5fac25ce5dd66884f054fc2777bc5759e Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Mon, 8 Dec 2025 15:29:07 +0100 Subject: [PATCH 09/30] add bound action test --- .../TravelServiceIntegrationTest.java | 24 +++++++++++++++++++ .../xtravels/TravelServiceODataTest.java | 23 ------------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 7250743..ab7e71a 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -261,6 +261,30 @@ void shouldDeleteTravel() throws Exception { } + @Test + @WithMockUser("admin") + void shouldExecuteAcceptTravelAction() throws Exception { + // First create a travel + Map travelData = createUniqueTravelData("shouldExecuteAcceptTravelAction"); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute acceptTravel action + mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.acceptTravel") + .contentType("application/json") + .content("{}")) + .andExpect(status().is2xxSuccessful()); + } + + private Map createUniqueTravelData(String testName) { synchronized (TravelServiceIntegrationTest.class) { diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java index a38a749..7e47b4d 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java @@ -76,29 +76,6 @@ void shouldUpdateTravel() throws Exception { // ========== Travel Actions Tests ========== - - @Test - void shouldExecuteAcceptTravelAction() throws Exception { - // First create a travel - Map travelData = createTravelData(); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Execute acceptTravel action - mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.acceptTravel") - .contentType("application/json") - .content("{}")) - .andExpect(status().isOk()); - } - @Test void shouldExecuteRejectTravelAction() throws Exception { // First create a travel From 33648237c8fdfae9973f679bad9ce18cdd8fccd7 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 09:23:15 +0100 Subject: [PATCH 10/30] actually assert travel accept action result --- package-lock.json | 71 ++++++++++++++++--- .../TravelServiceIntegrationTest.java | 8 ++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f08857..5a7b8c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -111,7 +112,6 @@ "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.5.1.tgz", "integrity": "sha512-rMvDSRytjqYQolB0pg8tiBlpS9kKGcleRhpZmBGUmSncbbwnotKYTKoDyMCWkflS8P9/Jq9YfY1qhK+fduHCVA==", "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@sap/cds-compiler": "^6.3", "@sap/cds-fiori": "^2", @@ -237,6 +237,7 @@ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -249,7 +250,6 @@ "integrity": "sha512-rMvDSRytjqYQolB0pg8tiBlpS9kKGcleRhpZmBGUmSncbbwnotKYTKoDyMCWkflS8P9/Jq9YfY1qhK+fduHCVA==", "dev": true, "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@sap/cds-compiler": "^6.3", "@sap/cds-fiori": "^2", @@ -1207,7 +1207,6 @@ "integrity": "sha512-XMUhfpsH99+I0WXRDnpNUYQx00ZiSceCusCF9Eo9+zgnOIdWYl5NP54hebYobe5CDEzQtSQXpud7+KoX3qTzMQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "iconv-lite": "0.7.0" }, @@ -2358,6 +2357,7 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", + "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -2402,7 +2402,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/assert-plus": { "version": "1.0.0", @@ -2465,6 +2466,7 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2524,6 +2526,7 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -2671,6 +2674,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -2683,6 +2687,7 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -2692,6 +2697,7 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -2700,7 +2706,8 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/core-util-is": { "version": "1.0.3", @@ -2714,6 +2721,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -2751,6 +2759,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -2760,6 +2769,7 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -2783,7 +2793,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2797,6 +2808,7 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -2871,13 +2883,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -2954,6 +2968,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -3033,6 +3048,7 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3042,6 +3058,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3186,6 +3203,7 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", + "peer": true, "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -3202,6 +3220,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -3220,6 +3239,7 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -3296,6 +3316,7 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3305,6 +3326,7 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -3314,6 +3336,7 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3323,6 +3346,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", + "peer": true, "bin": { "mime": "cli.js" }, @@ -3355,13 +3379,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3384,6 +3410,7 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -3396,6 +3423,7 @@ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -3418,6 +3446,7 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3426,7 +3455,8 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pend": { "version": "1.2.0", @@ -3457,6 +3487,7 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", + "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -3477,6 +3508,7 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "side-channel": "^1.0.6" }, @@ -3492,6 +3524,7 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -3501,6 +3534,7 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", + "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3575,6 +3609,7 @@ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -3599,6 +3634,7 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3607,13 +3643,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", + "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -3646,13 +3684,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -3672,6 +3712,7 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -3688,6 +3729,7 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3706,6 +3748,7 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3725,6 +3768,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3830,6 +3874,7 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.6" } @@ -3839,6 +3884,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", + "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3874,6 +3920,7 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -3890,6 +3937,7 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -3899,6 +3947,7 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index ab7e71a..aa98155 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -229,7 +229,7 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { Map actionParams = new HashMap<>(); actionParams.put("percent", 150); // Invalid percentage - mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.deductDiscount") + mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.deductDiscount") .contentType("application/json") .content(objectMapper.writeValueAsString(actionParams))) .andExpect(status().isBadRequest()); @@ -282,6 +282,12 @@ void shouldExecuteAcceptTravelAction() throws Exception { .contentType("application/json") .content("{}")) .andExpect(status().is2xxSuccessful()); + + // Check if travel status is accepted + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Status_code").value("A")); } From 2114d75845e8c9ab7e268b05199fbbab137df7ca Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 09:23:40 +0100 Subject: [PATCH 11/30] added http files --- test/http/TravelService.http | 275 +++++++++++++++++++++++++ test/http/sap.capire.flights.data.http | 33 +++ 2 files changed, 308 insertions(+) create mode 100644 test/http/TravelService.http create mode 100644 test/http/sap.capire.flights.data.http diff --git a/test/http/TravelService.http b/test/http/TravelService.http new file mode 100644 index 0000000..e001de4 --- /dev/null +++ b/test/http/TravelService.http @@ -0,0 +1,275 @@ +@server=http://localhost:8080 +@username=admin +@password= + + +### Travels +# @name Travels_GET +GET {{server}}/odata/v4/travel/Travels +Authorization: Basic {{username}}:{{password}} + + +### Travels Drafts GET +# @name Travels_Drafts_GET +GET {{server}}/odata/v4/travel/Travels?$filter=(IsActiveEntity eq false) +Authorization: Basic {{username}}:{{password}} + +### Travels Drafts GET +# @name Travels_Drafts_GET +GET http://localhost:8080/odata/v4/travel/Travels(ID=1,IsActiveEntity=true) +Authorization: Basic YWRtaW46 + + + +### Travels Draft POST +# @name Travels_Draft_POST +POST {{server}}/odata/v4/travel/Travels +Content-Type: application/json +Authorization: Basic {{username}}:{{password}} + +{ + "Description": "Description-23174596", + "BeginDate": "2018-07-31", + "EndDate": "2016-08-01", + "BookingFee": 90204.5092, + "TotalPrice": 81479.652, + "Currency": { + "code": "890" + }, + "Status": { + "code": "A" + }, + "Agency": { + "ID": "270904" + }, + "Customer": { + "ID": "588238" + }, + "Bookings": [ + { + "Pos": 16001352, + "Flight": { + "ID": "ts-20071832", + "date": "2004-01-03" + }, + "FlightPrice": 69100.9987, + "Currency": { + "code": "890" + }, + "Supplements": [ + { + "ID": "10036670-2fb0-4a00-89e0-c433979ff5b9", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 63025.421, + "Currency": { + "code": "890" + } + }, + { + "ID": "10036681-bc8d-4063-ae7e-2a7e2d222a49", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 50718.3963, + "Currency": { + "code": "890" + } + } + ], + "BookingDate": "2003-08-28" + }, + { + "Pos": 16001353, + "Flight": { + "ID": "ts-20071832", + "date": "2004-01-03" + }, + "FlightPrice": 49406.9219, + "Currency": { + "code": "890" + }, + "Supplements": [ + { + "ID": "18472766-b0be-456b-808f-3525c58cdb7a", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 81274.8795, + "Currency": { + "code": "890" + } + }, + { + "ID": "18472767-7033-4186-9ae4-eaa8d3d16780", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 40101.901, + "Currency": { + "code": "890" + } + } + ], + "BookingDate": "2018-04-23" + } + ], + "createdAt": "2012-02-16T23:00:00.000Z", + "createdBy": "createdBy.dspms@example.net", + "modifiedAt": "2023-12-29T23:00:00.000Z", + "modifiedBy": "modifiedBy.dspms@example.org" +} + + +### Result from POST request above +@draftID={{Travels_Draft_POST.response.body.$.ID}} + + +### Travels Draft PATCH +# @name Travels_Draft_Patch +PATCH {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false) +Content-Type: application/json +Authorization: Basic {{username}}:{{password}} + +{ + "Description": "Description-23174596", + "BeginDate": "2018-07-31", + "EndDate": "2016-08-01", + "BookingFee": 90204.5092, + "TotalPrice": 81479.652, + "Currency": { + "code": "890" + }, + "Status": { + "code": "A" + }, + "Agency": { + "ID": "270904" + }, + "Customer": { + "ID": "588238" + }, + "Bookings": [ + { + "Pos": 16001352, + "Flight": { + "ID": "ts-20071832", + "date": "2004-01-03" + }, + "FlightPrice": 69100.9987, + "Currency": { + "code": "890" + }, + "Supplements": [ + { + "ID": "10036670-2fb0-4a00-89e0-c433979ff5b9", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 63025.421, + "Currency": { + "code": "890" + } + }, + { + "ID": "10036681-bc8d-4063-ae7e-2a7e2d222a49", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 50718.3963, + "Currency": { + "code": "890" + } + } + ], + "BookingDate": "2003-08-28" + }, + { + "Pos": 16001353, + "Flight": { + "ID": "ts-20071832", + "date": "2004-01-03" + }, + "FlightPrice": 49406.9219, + "Currency": { + "code": "890" + }, + "Supplements": [ + { + "ID": "18472766-b0be-456b-808f-3525c58cdb7a", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 81274.8795, + "Currency": { + "code": "890" + } + }, + { + "ID": "18472767-7033-4186-9ae4-eaa8d3d16780", + "booked": { + "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" + }, + "Price": 40101.901, + "Currency": { + "code": "890" + } + } + ], + "BookingDate": "2018-04-23" + } + ], + "createdAt": "2012-02-16T23:00:00.000Z", + "createdBy": "createdBy.dspms@example.net", + "modifiedAt": "2023-12-29T23:00:00.000Z", + "modifiedBy": "modifiedBy.dspms@example.org" +} + + +### Travels Draft Prepare +# @name Travels_Draft_Prepare +POST {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false)/TravelService.draftPrepare +Content-Type: application/json +Authorization: Basic {{username}}:{{password}} + +{} + + +### Travels Draft Activate +# @name Travels_Draft_Activate +POST {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false)/TravelService.draftActivate +Content-Type: application/json +Authorization: Basic {{username}}:{{password}} + +{} + + +### Currencies +# @name Currencies_GET +GET {{server}}/odata/v4/travel/Currencies +Authorization: Basic {{username}}:{{password}} + + +### TravelStatus +# @name TravelStatus_GET +GET {{server}}/odata/v4/travel/TravelStatus +Authorization: Basic {{username}}:{{password}} + + +### Flights +# @name Flights_GET +GET {{server}}/odata/v4/travel/Flights +Authorization: Basic {{username}}:{{password}} + + +### Supplements +# @name Supplements_GET +GET {{server}}/odata/v4/travel/Supplements +Authorization: Basic {{username}}:{{password}} + + +### SupplementTypes +# @name SupplementTypes_GET +GET {{server}}/odata/v4/travel/SupplementTypes +Authorization: Basic {{username}}:{{password}} diff --git a/test/http/sap.capire.flights.data.http b/test/http/sap.capire.flights.data.http new file mode 100644 index 0000000..4a84f98 --- /dev/null +++ b/test/http/sap.capire.flights.data.http @@ -0,0 +1,33 @@ +@server=http://localhost:8080 +@username=admin +@password= + + +### Flights +# @name Flights_GET +GET {{server}}/odata/v4/sap.capire.flights.data/Flights +Authorization: Basic {{username}}:{{password}} + + +### Airlines +# @name Airlines_GET +GET {{server}}/odata/v4/sap.capire.flights.data/Airlines +Authorization: Basic {{username}}:{{password}} + + +### Airports +# @name Airports_GET +GET {{server}}/odata/v4/sap.capire.flights.data/Airports +Authorization: Basic {{username}}:{{password}} + + +### Supplements +# @name Supplements_GET +GET {{server}}/odata/v4/sap.capire.flights.data/Supplements +Authorization: Basic {{username}}:{{password}} + + +### SupplementTypes +# @name SupplementTypes_GET +GET {{server}}/odata/v4/sap.capire.flights.data/SupplementTypes +Authorization: Basic {{username}}:{{password}} From f8e24af01cf923893dfe9a797cebdf2d60a6115d Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 11:30:11 +0100 Subject: [PATCH 12/30] add recalculate price logic for travels without bookings --- .../handler/RecalculatePriceHandler.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java index 3f55b09..fd98d35 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java @@ -1,7 +1,5 @@ package sap.capire.xtravels.handler; -import static cds.gen.travelservice.TravelService_.TRAVELS; - import cds.gen.travelservice.Bookings; import cds.gen.travelservice.Bookings_; import cds.gen.travelservice.TravelService; @@ -21,9 +19,15 @@ import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.After; import com.sap.cds.services.handler.annotations.ServiceName; +import org.springframework.stereotype.Component; + import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; -import org.springframework.stereotype.Component; + +import static cds.gen.travelservice.TravelService_.TRAVELS; +import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; // Update a Travel's TotalPrice whenever its BookingFee is modified, // or when a nested Booking is deleted or its FlightPrice is modified, @@ -67,6 +71,20 @@ private void updateTotals(CqnStructuredTypeRef ref) { Update.entity(travel).data(Travels.TOTAL_PRICE, totalPrice).hint("@readonly", false)); } + @After(event = EVENT_CREATE, entity = Travels_.CDS_NAME) + void setTotalPriceAfterCreation(Travels travels) { + + //travel is created with the total price being the booking fee in case no total price is set + if (travels.getTotalPrice() == null || travels.getTotalPrice().equals(BigDecimal.ZERO)) { + Map updateData = new HashMap<>(); + updateData.put(Travels.ID, travels.getId()); + updateData.put(Travels.TOTAL_PRICE, travels.getBookingFee()); + + service.run( + Update.entity(Travels_.class).data(updateData).hint("@readonly", false)); + } + } + private Value orZero(Value value) { return CQL.func("coalesce", value, CQL.constant(0)); } From 323250b04bb76e55bfcd4b87634e1d819ecad326 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 11:31:47 +0100 Subject: [PATCH 13/30] add more tests for actions --- .../TravelServiceIntegrationTest.java | 77 ++++++- .../xtravels/TravelServiceODataTest.java | 200 ------------------ 2 files changed, 67 insertions(+), 210 deletions(-) delete mode 100644 srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index aa98155..15b40db 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -44,9 +44,6 @@ class TravelServiceIntegrationTest { private static final String ODATA_BASE_URL = "/odata/v4/travel"; private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels"; - private static final String FLIGHTS_ENDPOINT = ODATA_BASE_URL + "/Flights"; - private static final String SUPPLEMENTS_ENDPOINT = ODATA_BASE_URL + "/Supplements"; - private static final String CURRENCIES_ENDPOINT = ODATA_BASE_URL + "/Currencies"; private static int testCounter = 0; // for test-data generation @@ -95,7 +92,7 @@ void shouldGetAllTravels() throws Exception { @Test @WithMockUser("admin") void shouldCreateTravel() throws Exception { - Map travelData = createUniqueTravelData("shouldCreateTravel"); + Map travelData = createTravelData("shouldCreateTravel"); mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") @@ -114,7 +111,7 @@ void shouldCreateTravel() throws Exception { @WithMockUser("admin") void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { - Map travelData = createUniqueTravelData("shouldCreateAndRetrieveTravelSuccessfully"); + Map travelData = createTravelData("shouldCreateAndRetrieveTravelSuccessfully"); travelData.put("BookingFee", 200.0); travelData.put("Currency_code", "USD"); @@ -213,7 +210,7 @@ void shouldReturn404ForNonExistentEntity() throws Exception { @WithMockUser("admin") void shouldReturn400ForInvalidDiscountPercentage() throws Exception { // First create a travel - Map travelData = createUniqueTravelData("shouldReturn400ForInvalidDiscountPercentage"); + Map travelData = createTravelData("shouldReturn400ForInvalidDiscountPercentage"); String response = mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") .content(objectMapper.writeValueAsString(travelData))) @@ -239,7 +236,7 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { @WithMockUser("admin") void shouldDeleteTravel() throws Exception { // First create a travel - Map travelData = createUniqueTravelData("shouldDeleteTravel"); + Map travelData = createTravelData("shouldDeleteTravel"); String response = mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") .content(objectMapper.writeValueAsString(travelData))) @@ -265,7 +262,7 @@ void shouldDeleteTravel() throws Exception { @WithMockUser("admin") void shouldExecuteAcceptTravelAction() throws Exception { // First create a travel - Map travelData = createUniqueTravelData("shouldExecuteAcceptTravelAction"); + Map travelData = createTravelData("shouldExecuteAcceptTravelAction"); String response = mockMvc.perform(post(TRAVELS_ENDPOINT) .contentType("application/json") .content(objectMapper.writeValueAsString(travelData))) @@ -290,9 +287,69 @@ void shouldExecuteAcceptTravelAction() throws Exception { .andExpect(jsonPath("$.Status_code").value("A")); } + @Test + @WithMockUser("admin") + void shouldExecuteRejectTravelAction() throws Exception { + // First create a travel + Map travelData = createTravelData("shouldExecuteRejectTravelAction"); + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute rejectTravel action + mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.rejectTravel") + .contentType("application/json") + .content("{}")) + .andExpect(status().is2xxSuccessful()); + + // Check if travel status is rejected + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Status_code").value("X")); + } + @Test + @WithMockUser("admin") + void shouldExecuteDeductDiscountAction() throws Exception { + // First create a travel + Map travelData = createTravelData("shouldExecuteDeductDiscountAction"); + + String response = mockMvc.perform(post(TRAVELS_ENDPOINT) + .contentType("application/json") + .content(objectMapper.writeValueAsString(travelData))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Map createdTravel = objectMapper.readValue(response, Map.class); + Integer travelId = (Integer) createdTravel.get("ID"); + Integer deduction = 10; + + // Execute deductDiscount action with 10% discount + Map actionParams = new HashMap<>(); + actionParams.put("percent", deduction); + + mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.deductDiscount") + .contentType("application/json") + .content(objectMapper.writeValueAsString(actionParams))) + .andExpect(status().isOk()); + + mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.BookingFee").value(90)); + } - private Map createUniqueTravelData(String testName) { + private Map createTravelData(String testName) { synchronized (TravelServiceIntegrationTest.class) { testCounter++; } @@ -302,7 +359,7 @@ private Map createUniqueTravelData(String testName) { travelData.put("Description", testName + " - Test Travel " + testCounter + " to Paris"); travelData.put("BeginDate", LocalDate.now().plusDays(30 + testCounter).toString()); travelData.put("EndDate", LocalDate.now().plusDays(37 + testCounter).toString()); - travelData.put("BookingFee", 100 + testCounter); + travelData.put("BookingFee", 100); travelData.put("Currency_code", "EUR"); travelData.put("Agency_ID", "070001"); travelData.put("Customer_ID", "000001"); diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java deleted file mode 100644 index 7e47b4d..0000000 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceODataTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package sap.capire.xtravels; - - - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.WebApplicationContext; - -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; - -@SpringBootTest -@AutoConfigureWebMvc -class TravelServiceODataTest { - /* - - @Autowired - private WebApplicationContext webApplicationContext; - - @Autowired - private ObjectMapper objectMapper; - - - - - - // ========== Service Document and Metadata Tests ========== - - - - - - @Test - void shouldUpdateTravel() throws Exception { - // First create a travel - Map travelData = createTravelData(); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Update the travel - Map updateData = new HashMap<>(); - updateData.put("Description", "Updated Travel Description"); - updateData.put("BookingFee", 150.0); - - mockMvc.perform(patch(TRAVELS_ENDPOINT + "(" + travelId + ")") - .contentType("application/json") - .content(objectMapper.writeValueAsString(updateData))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.Description", is("Updated Travel Description"))) - .andExpect(jsonPath("$.BookingFee", is(150.0))); - } - - // ========== OData Query Options Tests ========== - - - - - - - // ========== Travel Actions Tests ========== - @Test - void shouldExecuteRejectTravelAction() throws Exception { - // First create a travel - Map travelData = createTravelData(); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Execute rejectTravel action - mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.rejectTravel") - .contentType("application/json") - .content("{}")) - .andExpect(status().isOk()); - } - - @Test - void shouldExecuteDeductDiscountAction() throws Exception { - // First create a travel - Map travelData = createTravelData(); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Execute deductDiscount action with 10% discount - Map actionParams = new HashMap<>(); - actionParams.put("percent", 10); - - mockMvc.perform(post(TRAVELS_ENDPOINT + "(" + travelId + ")/TravelService.deductDiscount") - .contentType("application/json") - .content(objectMapper.writeValueAsString(actionParams))) - .andExpect(status().isOk()); - } - - // ========== Read-only Entities Tests ========== - - @Test - void shouldGetAllFlights() throws Exception { - mockMvc.perform(get(FLIGHTS_ENDPOINT)) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.@odata.context", containsString("Flights"))) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - - @Test - void shouldGetAllSupplements() throws Exception { - mockMvc.perform(get(SUPPLEMENTS_ENDPOINT)) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.@odata.context", containsString("Supplements"))) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - - @Test - void shouldGetAllCurrencies() throws Exception { - mockMvc.perform(get(CURRENCIES_ENDPOINT)) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.@odata.context", containsString("Currencies"))) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - - @Test - void shouldNotAllowPostToReadOnlyFlights() throws Exception { - Map flightData = new HashMap<>(); - flightData.put("PlaneType", "Boeing 747"); - - mockMvc.perform(post(FLIGHTS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(flightData))) - .andExpect(status().isMethodNotAllowed()); - } - - @Test - void shouldNotAllowPutToReadOnlySupplements() throws Exception { - Map supplementData = new HashMap<>(); - supplementData.put("Description", "Premium Meal"); - - mockMvc.perform(put(SUPPLEMENTS_ENDPOINT + "(1)") - .contentType("application/json") - .content(objectMapper.writeValueAsString(supplementData))) - .andExpect(status().isMethodNotAllowed()); - } - - // ========== Error Handling Tests ========== - - @Test - void shouldReturn400ForInvalidTravelData() throws Exception { - Map invalidData = new HashMap<>(); - invalidData.put("BeginDate", "invalid-date"); - - mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(invalidData))) - .andExpect(status().isBadRequest()); - } - - @Test - void shouldReturn404ForNonExistentTravel() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "(99999)")) - .andExpect(status().isNotFound()); - } - - - - // ========== Helper Methods ========== - */ -} From e1e28aef60a912575324f24eb834da99e13b7381 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 9 Dec 2025 15:07:40 +0100 Subject: [PATCH 14/30] Update srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java Co-authored-by: David H Lam --- .../sap/capire/xtravels/handler/RecalculatePriceHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java index fd98d35..d3678e7 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java @@ -71,7 +71,7 @@ private void updateTotals(CqnStructuredTypeRef ref) { Update.entity(travel).data(Travels.TOTAL_PRICE, totalPrice).hint("@readonly", false)); } - @After(event = EVENT_CREATE, entity = Travels_.CDS_NAME) + @After(event = EVENT_CREATE) void setTotalPriceAfterCreation(Travels travels) { //travel is created with the total price being the booking fee in case no total price is set From f6bee47300df13f649ffb2fafca2257a2573032e Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 9 Dec 2025 15:09:29 +0100 Subject: [PATCH 15/30] Update test/http/TravelService.http Co-authored-by: David H Lam --- test/http/TravelService.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/http/TravelService.http b/test/http/TravelService.http index e001de4..07dc885 100644 --- a/test/http/TravelService.http +++ b/test/http/TravelService.http @@ -17,7 +17,7 @@ Authorization: Basic {{username}}:{{password}} ### Travels Drafts GET # @name Travels_Drafts_GET GET http://localhost:8080/odata/v4/travel/Travels(ID=1,IsActiveEntity=true) -Authorization: Basic YWRtaW46 +Authorization: Basic {{username}}:{{password}} From cf183a8b6797ddfdd90b3c3117f3e733950af7df Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 15:17:40 +0100 Subject: [PATCH 16/30] assert currency_code and booking fee after creation --- .../sap/capire/xtravels/TravelServiceIntegrationTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 15b40db..996a1c5 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -131,7 +131,9 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) + .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))); } @Test From 3ee53cae18eafe4f1d0053dab3659ad0f3adcf24 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 16:14:16 +0100 Subject: [PATCH 17/30] asssert that currency code is EUR --- .../sap/capire/xtravels/TravelServiceIntegrationTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 996a1c5..8763557 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -140,9 +140,10 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { @WithMockUser("admin") void shouldSupportODataFilterQuery() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); + mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'&$top=1&$skip=0")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.value[0].Currency_code", is("EUR"))); } @Test From a20386809c296b650aec9f6a9f136e5554df150d Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 16:21:14 +0100 Subject: [PATCH 18/30] assert the ordering of the result --- db/data/sap.capire.travels-Travels.csv | 4 ++-- .../sap/capire/xtravels/TravelServiceIntegrationTest.java | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/db/data/sap.capire.travels-Travels.csv b/db/data/sap.capire.travels-Travels.csv index a4bad44..4cd90c8 100644 --- a/db/data/sap.capire.travels-Travels.csv +++ b/db/data/sap.capire.travels-Travels.csv @@ -762,8 +762,8 @@ ID,Description,BeginDate,EndDate,BookingFee,TotalPrice,Currency_code,Status_code 761,Visiting Horst,2024-05-29,2024-05-29,20,459,USD,A,070033,000506,2023-10-03T02:54:53.000Z,Neubasler,2023-10-16T01:35:57.000Z,Barth 762,Vacation for Sophie,2024-05-29,2024-05-29,10,248,USD,O,070015,000239,2023-10-03T16:13:08.000Z,Columbo,2023-10-09T00:56:54.000Z,Eichbaum 763,Vacation,2024-05-29,2024-05-29,10,243,USD,O,070004,000038,2023-10-03T21:22:40.000Z,Ryan,2023-10-04T17:28:22.000Z,Rahn -764,Visiting Walter,2024-05-29,2024-05-29,10,260,USD,A,070031,000478,2023-10-03T20:15:42.000Z,Meier,2023-10-15T20:31:31.000Z,Meier -765,Vacation to Cuba,2024-05-29,2024-05-29,20,499,USD,X,070024,000080,2023-10-03T14:26:00.000Z,Kramer,2023-10-12T11:04:49.000Z,Sessler +764,Watching Walter,2024-05-29,2024-05-29,10,260,USD,A,070031,000478,2023-10-03T20:15:42.000Z,Meier,2023-10-15T20:31:31.000Z,Meier +765,Zazzing the Cuba,2024-05-29,2024-05-29,20,499,USD,X,070024,000080,2023-10-03T14:26:00.000Z,Kramer,2023-10-12T11:04:49.000Z,Sessler 766,"Vacation for Johann, Laura, Roland",2024-05-29,2024-05-29,30,782,USD,O,070014,000680,2023-10-03T07:01:44.000Z,Martin,2023-10-08T01:10:40.000Z,Henry 767,Vacation,2024-05-29,2024-05-29,10,265,USD,O,070050,000384,2023-10-03T14:25:34.000Z,Pan,2023-10-23T18:57:55.000Z,Domenech 768,Vacation,2024-05-29,2024-05-29,30,835,USD,O,070042,000194,2023-10-03T14:45:50.000Z,Henry,2023-10-20T07:57:01.000Z,Domenech diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 8763557..12ef4f5 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -158,10 +158,13 @@ void shouldSupportSelectQuery() throws Exception { @Test @WithMockUser("admin") void shouldSupportOrderByQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc")) + + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc&$top=3&$skip=0")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); + .andExpect(jsonPath("$.value[0].Description", is("Zazzing the Cuba"))) + .andExpect(jsonPath("$.value[1].Description", is("Watching Walter"))) + .andExpect(jsonPath("$.value[2].Description", is("Visiting Walter"))); } @Test From 6a910a96ce925d2fdabecb87a13e641c7aebf240 Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 16:23:27 +0100 Subject: [PATCH 19/30] assert size of results --- .../java/sap/capire/xtravels/TravelServiceIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 12ef4f5..cdf0b59 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -152,7 +152,7 @@ void shouldSupportSelectQuery() throws Exception { mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); + .andExpect(jsonPath("$.value.length()", is(1000))); } @Test From 998a2fafeeb6977d1d6503972990acea50d6bf7a Mon Sep 17 00:00:00 2001 From: Robin de Silva Jayasinghe Date: Tue, 9 Dec 2025 16:30:17 +0100 Subject: [PATCH 20/30] use more stable order criteria --- .../sap/capire/xtravels/TravelServiceIntegrationTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index cdf0b59..d02bf85 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -159,12 +159,12 @@ void shouldSupportSelectQuery() throws Exception { @WithMockUser("admin") void shouldSupportOrderByQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Description desc&$top=3&$skip=0")) + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Currency_code desc&$top=3&$skip=0")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value[0].Description", is("Zazzing the Cuba"))) - .andExpect(jsonPath("$.value[1].Description", is("Watching Walter"))) - .andExpect(jsonPath("$.value[2].Description", is("Visiting Walter"))); + .andExpect(jsonPath("$.value[0].Currency_code", is("USD"))) + .andExpect(jsonPath("$.value[1].Currency_code", is("USD"))) + .andExpect(jsonPath("$.value[2].Currency_code", is("USD"))); } @Test From 165750f3c33ecabc06ab3e34cd7bbde35c90f61c Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Wed, 10 Dec 2025 11:16:03 +0100 Subject: [PATCH 21/30] Improved test expectations --- .../TravelServiceIntegrationTest.java | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index d02bf85..b59559a 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -2,6 +2,13 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,25 +16,19 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; -import static org.hamcrest.Matchers.matchesRegex; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Integration tests for the CAP Travel Service OData endpoints. @@ -70,7 +71,6 @@ void contextLoads() { @Test @WithMockUser("admin") void shouldGetMetadataSuccessfully() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/$metadata")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/xml")); @@ -110,7 +110,6 @@ void shouldCreateTravel() throws Exception { @Test @WithMockUser("admin") void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { - Map travelData = createTravelData("shouldCreateAndRetrieveTravelSuccessfully"); travelData.put("BookingFee", 200.0); travelData.put("Currency_code", "USD"); @@ -139,61 +138,74 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { @Test @WithMockUser("admin") void shouldSupportODataFilterQuery() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'&$top=1&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.value[0].Currency_code", is("EUR"))); + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.value[0].Currency_code", is("EUR"))); } @Test @WithMockUser("admin") void shouldSupportSelectQuery() throws Exception { mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value.length()", is(1000))); + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value.length()", is(1000))) + .andExpect(jsonPath("$.value[0].length()", is(5))) + .andExpect(jsonPath("$.value[0]", hasKey("ID"))) + .andExpect(jsonPath("$.value[0]", hasKey("Description"))) + .andExpect(jsonPath("$.value[0]", hasKey("Currency_code"))); } @Test @WithMockUser("admin") void shouldSupportOrderByQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Currency_code desc&$top=3&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value[0].Currency_code", is("USD"))) - .andExpect(jsonPath("$.value[1].Currency_code", is("USD"))) - .andExpect(jsonPath("$.value[2].Currency_code", is("USD"))); + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value[0].Currency_code", is("USD"))) + .andExpect(jsonPath("$.value[1].Currency_code", is("USD"))) + .andExpect(jsonPath("$.value[2].Currency_code", is("USD"))); } @Test @WithMockUser("admin") void shouldSupportTopAndSkipQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=5&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=3&$skip=0")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(List.class))) + .andExpect(jsonPath("$.value.length()", is(3))) + .andExpect(jsonPath("$.value[0].ID", is(1))) + .andExpect(jsonPath("$.value[1].ID", is(2))) + .andExpect(jsonPath("$.value[2].ID", is(3))); + + mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=2&$skip=1")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) + .andExpect(jsonPath("$.value", isA(List.class))) + .andExpect(jsonPath("$.value.length()", is(2))) + .andExpect(jsonPath("$.value[0].ID", is(2))) + .andExpect(jsonPath("$.value[1].ID", is(3))); } @Test @WithMockUser("admin") void shouldSupportCountQuery() throws Exception { mockMvc.perform(get(TRAVELS_ENDPOINT + "/$count")) - .andExpect(status().isOk()) - .andExpect(content().string(matchesRegex("\\d+"))); + .andExpect(status().isOk()) + .andExpect(content().string(matchesRegex("\\d+"))); } @Test @WithMockUser("admin") void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { - // Test Flights entity mockMvc.perform(get(ODATA_BASE_URL + "/Flights")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")); - // Test Supplements entity + // Test Supplements entity mockMvc.perform(get(ODATA_BASE_URL + "/Supplements")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")); @@ -207,7 +219,6 @@ void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { @Test @WithMockUser("admin") void shouldReturn404ForNonExistentEntity() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID=9999999,IsActiveEntity=false)")) .andExpect(status().isNotFound()); } From 887dec87aa914390b6d3be14ec772a663af01c66 Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Tue, 16 Dec 2025 13:54:24 +0100 Subject: [PATCH 22/30] Added test scope --- srv/pom.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/srv/pom.xml b/srv/pom.xml index 6bf7c1d..63c219e 100644 --- a/srv/pom.xml +++ b/srv/pom.xml @@ -83,10 +83,11 @@ spring-boot-starter-test test - - org.springframework.security - spring-security-test - + + org.springframework.security + spring-security-test + test + From eebfa3bf049274751f3f4c57027b497cda8aa58e Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Tue, 16 Dec 2025 14:27:36 +0100 Subject: [PATCH 23/30] Removing handler code --- .../handler/RecalculatePriceHandler.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java index d3678e7..b81a093 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java @@ -22,12 +22,9 @@ import org.springframework.stereotype.Component; import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; import java.util.function.Function; import static cds.gen.travelservice.TravelService_.TRAVELS; -import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; // Update a Travel's TotalPrice whenever its BookingFee is modified, // or when a nested Booking is deleted or its FlightPrice is modified, @@ -71,20 +68,6 @@ private void updateTotals(CqnStructuredTypeRef ref) { Update.entity(travel).data(Travels.TOTAL_PRICE, totalPrice).hint("@readonly", false)); } - @After(event = EVENT_CREATE) - void setTotalPriceAfterCreation(Travels travels) { - - //travel is created with the total price being the booking fee in case no total price is set - if (travels.getTotalPrice() == null || travels.getTotalPrice().equals(BigDecimal.ZERO)) { - Map updateData = new HashMap<>(); - updateData.put(Travels.ID, travels.getId()); - updateData.put(Travels.TOTAL_PRICE, travels.getBookingFee()); - - service.run( - Update.entity(Travels_.class).data(updateData).hint("@readonly", false)); - } - } - private Value orZero(Value value) { return CQL.func("coalesce", value, CQL.constant(0)); } From 97aa9dd7272be6bbf9ccc58fe33b95a47840d483 Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Tue, 16 Dec 2025 14:28:57 +0100 Subject: [PATCH 24/30] Import order --- .../sap/capire/xtravels/handler/RecalculatePriceHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java index b81a093..c4b61e6 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java @@ -1,5 +1,7 @@ package sap.capire.xtravels.handler; +import static cds.gen.travelservice.TravelService_.TRAVELS; + import cds.gen.travelservice.Bookings; import cds.gen.travelservice.Bookings_; import cds.gen.travelservice.TravelService; @@ -20,11 +22,9 @@ import com.sap.cds.services.handler.annotations.After; import com.sap.cds.services.handler.annotations.ServiceName; import org.springframework.stereotype.Component; - import java.math.BigDecimal; import java.util.function.Function; -import static cds.gen.travelservice.TravelService_.TRAVELS; // Update a Travel's TotalPrice whenever its BookingFee is modified, // or when a nested Booking is deleted or its FlightPrice is modified, From a309a1534b790ea5944da4e3f8a2b74547c38fa4 Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Tue, 16 Dec 2025 14:29:35 +0100 Subject: [PATCH 25/30] Import order --- .../sap/capire/xtravels/handler/RecalculatePriceHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java index c4b61e6..3f55b09 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/RecalculatePriceHandler.java @@ -21,10 +21,9 @@ import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.After; import com.sap.cds.services.handler.annotations.ServiceName; -import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.function.Function; - +import org.springframework.stereotype.Component; // Update a Travel's TotalPrice whenever its BookingFee is modified, // or when a nested Booking is deleted or its FlightPrice is modified, From 96740716b58cf28ee562a0d278e853ba1b8d2f9c Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Wed, 17 Dec 2025 11:40:37 +0100 Subject: [PATCH 26/30] cds-services 4.6.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8d824ed..5dea50d 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 21 - 4.5.1 + 4.6.0 3.5.8 5.24.0 @@ -169,4 +169,4 @@ - \ No newline at end of file + From 96972d35d113d8fd9c092a9f2feaaeb66e6de509 Mon Sep 17 00:00:00 2001 From: "David H. Lam" Date: Wed, 17 Dec 2025 11:41:51 +0100 Subject: [PATCH 27/30] Dealing with that TotalPrice could be null --- .../xtravels/handler/DeductDiscountHandler.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java index bf00c35..93b8966 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java @@ -35,14 +35,17 @@ Travels deductDiscount(Travels_ ref, final TravelsDeductDiscountContext context) .getBookingFee() .subtract(travel.getBookingFee().multiply(discount)) .round(new MathContext(3))); - travel.setTotalPrice( - travel - .getTotalPrice() - .subtract(travel.getTotalPrice().multiply(discount)) - .round(new MathContext(3))); + BigDecimal totalPrice = travel.getTotalPrice(); + + if (totalPrice != null && totalPrice.compareTo(BigDecimal.ZERO) > 0) { + travel.setTotalPrice( + totalPrice + .subtract(totalPrice.multiply(discount)) + .round(new MathContext(3))); + } Travels update = Travels.create(); - update.setTotalPrice(travel.getTotalPrice()); + update.setTotalPrice(totalPrice); update.setBookingFee(travel.getBookingFee()); service.run(Update.entity(ref).data(update).hint("@readonly", false)); From 6c56b5965356f85699a55806b1870674fec86778 Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Wed, 7 Jan 2026 13:50:48 +0100 Subject: [PATCH 28/30] Simplify --- db/data/sap.capire.travels-Travels.csv | 4 +- db/schema.cds | 2 +- package-lock.json | 56 +- pom.xml | 2 +- srv/pom.xml | 1 + .../handler/DeductDiscountHandler.java | 15 +- .../TravelServiceIntegrationTest.java | 629 ++++++++---------- test/http/TravelService.http | 275 -------- test/http/sap.capire.flights.data.http | 33 - 9 files changed, 284 insertions(+), 733 deletions(-) delete mode 100644 test/http/TravelService.http delete mode 100644 test/http/sap.capire.flights.data.http diff --git a/db/data/sap.capire.travels-Travels.csv b/db/data/sap.capire.travels-Travels.csv index 4cd90c8..a4bad44 100644 --- a/db/data/sap.capire.travels-Travels.csv +++ b/db/data/sap.capire.travels-Travels.csv @@ -762,8 +762,8 @@ ID,Description,BeginDate,EndDate,BookingFee,TotalPrice,Currency_code,Status_code 761,Visiting Horst,2024-05-29,2024-05-29,20,459,USD,A,070033,000506,2023-10-03T02:54:53.000Z,Neubasler,2023-10-16T01:35:57.000Z,Barth 762,Vacation for Sophie,2024-05-29,2024-05-29,10,248,USD,O,070015,000239,2023-10-03T16:13:08.000Z,Columbo,2023-10-09T00:56:54.000Z,Eichbaum 763,Vacation,2024-05-29,2024-05-29,10,243,USD,O,070004,000038,2023-10-03T21:22:40.000Z,Ryan,2023-10-04T17:28:22.000Z,Rahn -764,Watching Walter,2024-05-29,2024-05-29,10,260,USD,A,070031,000478,2023-10-03T20:15:42.000Z,Meier,2023-10-15T20:31:31.000Z,Meier -765,Zazzing the Cuba,2024-05-29,2024-05-29,20,499,USD,X,070024,000080,2023-10-03T14:26:00.000Z,Kramer,2023-10-12T11:04:49.000Z,Sessler +764,Visiting Walter,2024-05-29,2024-05-29,10,260,USD,A,070031,000478,2023-10-03T20:15:42.000Z,Meier,2023-10-15T20:31:31.000Z,Meier +765,Vacation to Cuba,2024-05-29,2024-05-29,20,499,USD,X,070024,000080,2023-10-03T14:26:00.000Z,Kramer,2023-10-12T11:04:49.000Z,Sessler 766,"Vacation for Johann, Laura, Roland",2024-05-29,2024-05-29,30,782,USD,O,070014,000680,2023-10-03T07:01:44.000Z,Martin,2023-10-08T01:10:40.000Z,Henry 767,Vacation,2024-05-29,2024-05-29,10,265,USD,O,070050,000384,2023-10-03T14:25:34.000Z,Pan,2023-10-23T18:57:55.000Z,Domenech 768,Vacation,2024-05-29,2024-05-29,30,835,USD,O,070042,000194,2023-10-03T14:45:50.000Z,Henry,2023-10-20T07:57:01.000Z,Domenech diff --git a/db/schema.cds b/db/schema.cds index 53e934a..e22b261 100644 --- a/db/schema.cds +++ b/db/schema.cds @@ -13,7 +13,7 @@ entity Travels : managed { BeginDate : Date default $now; EndDate : Date default $now; BookingFee : Price default 0; - TotalPrice : Price @readonly; + TotalPrice : Price default 0 @readonly; Currency : Currency default 'EUR'; Status : Association to TravelStatus default 'O'; Agency : Association to TravelAgencies; diff --git a/package-lock.json b/package-lock.json index 51ce434..5da7060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2323,7 +2323,6 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -2368,8 +2367,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/assert-plus": { "version": "1.0.0", @@ -2432,7 +2430,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -2519,7 +2516,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -2654,7 +2650,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -2667,7 +2662,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2677,7 +2671,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2701,7 +2694,6 @@ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2747,7 +2739,6 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -2757,7 +2748,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -2781,8 +2771,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2796,7 +2785,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -2871,15 +2859,13 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2970,7 +2956,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -3065,7 +3050,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3075,7 +3059,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3220,7 +3203,6 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", - "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -3242,7 +3224,6 @@ "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -3261,7 +3242,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.10" } @@ -3338,7 +3318,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3348,7 +3327,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -3358,7 +3336,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3368,7 +3345,6 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", - "peer": true, "bin": { "mime": "cli.js" }, @@ -3408,7 +3384,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3431,7 +3406,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -3444,7 +3418,6 @@ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", - "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -3467,7 +3440,6 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -3476,8 +3448,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pend": { "version": "1.2.0", @@ -3508,7 +3479,6 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", - "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -3529,7 +3499,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -3545,7 +3514,6 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3555,7 +3523,6 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -3642,7 +3609,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -3682,7 +3648,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -3715,15 +3680,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -3743,7 +3706,6 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -3760,7 +3722,6 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3779,7 +3740,6 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3799,7 +3759,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -3905,7 +3864,6 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.6" } @@ -3915,7 +3873,6 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", - "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3951,7 +3908,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -3968,7 +3924,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -3978,7 +3933,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } diff --git a/pom.xml b/pom.xml index 34f5f0d..7dd167b 100644 --- a/pom.xml +++ b/pom.xml @@ -169,4 +169,4 @@ - + \ No newline at end of file diff --git a/srv/pom.xml b/srv/pom.xml index 63c219e..af6b087 100644 --- a/srv/pom.xml +++ b/srv/pom.xml @@ -83,6 +83,7 @@ spring-boot-starter-test test + org.springframework.security spring-security-test diff --git a/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java index 93b8966..bf00c35 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/DeductDiscountHandler.java @@ -35,17 +35,14 @@ Travels deductDiscount(Travels_ ref, final TravelsDeductDiscountContext context) .getBookingFee() .subtract(travel.getBookingFee().multiply(discount)) .round(new MathContext(3))); - BigDecimal totalPrice = travel.getTotalPrice(); - - if (totalPrice != null && totalPrice.compareTo(BigDecimal.ZERO) > 0) { - travel.setTotalPrice( - totalPrice - .subtract(totalPrice.multiply(discount)) - .round(new MathContext(3))); - } + travel.setTotalPrice( + travel + .getTotalPrice() + .subtract(travel.getTotalPrice().multiply(discount)) + .round(new MathContext(3))); Travels update = Travels.create(); - update.setTotalPrice(totalPrice); + update.setTotalPrice(travel.getTotalPrice()); update.setBookingFee(travel.getBookingFee()); service.run(Update.entity(ref).data(update).hint("@readonly", false)); diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index b59559a..8b7a06c 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -1,385 +1,292 @@ package sap.capire.xtravels; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; -import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import cds.gen.travelservice.Travels; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.CdsData; +import java.math.BigDecimal; import java.time.LocalDate; -import java.util.HashMap; -import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; -/** - * Integration tests for the CAP Travel Service OData endpoints. - * This class provides comprehensive testing of OData operations including: - * - Basic CRUD operations on Travels entity - * - OData query options ($filter, $select, $orderby, etc.) - * - Custom actions (acceptTravel, rejectTravel, deductDiscount) - * - Read-only entities (Flights, Supplements, Currencies) - * - Error handling scenarios - */ +/** Integration tests for the CAP Travel Service OData endpoints */ @SpringBootTest -@AutoConfigureWebMvc +@AutoConfigureMockMvc class TravelServiceIntegrationTest { - private static final String ODATA_BASE_URL = "/odata/v4/travel"; - private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels"; - - private static int testCounter = 0; // for test-data generation - - private MockMvc mockMvc; - - @Autowired - private WebApplicationContext webApplicationContext; - - @Autowired - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - mockMvc = MockMvcBuilders - .webAppContextSetup(webApplicationContext) - .build(); - } - - @Test - void contextLoads() { - assertNotNull(webApplicationContext); - } - - @Test - @WithMockUser("admin") - void shouldGetMetadataSuccessfully() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/$metadata")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/xml")); - } - - // ========== Travels Entity Tests ========== - - @Test - @WithMockUser("admin") - void shouldGetAllTravels() throws Exception { - - mockMvc.perform(get(TRAVELS_ENDPOINT)) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.@context", containsString("Travels"))) - .andExpect(jsonPath("$.value", isA(java.util.List.class))); - } - - @Test - @WithMockUser("admin") - void shouldCreateTravel() throws Exception { - Map travelData = createTravelData("shouldCreateTravel"); - - mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.Description", is(travelData.get("Description")))) - .andExpect(jsonPath("$.BeginDate", is(travelData.get("BeginDate")))) - .andExpect(jsonPath("$.EndDate", is(travelData.get("EndDate")))) - .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) - .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))) - .andExpect(jsonPath("$.ID", notNullValue())); - } - - @Test - @WithMockUser("admin") - void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { - Map travelData = createTravelData("shouldCreateAndRetrieveTravelSuccessfully"); - travelData.put("BookingFee", 200.0); - travelData.put("Currency_code", "USD"); - - // Create travel - String response = mockMvc.perform(post(ODATA_BASE_URL + "/Travels") + private static final String ODATA_BASE_URL = "/odata/v4/travel"; + private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels"; + + @Autowired private MockMvc mockMvc; + + @Test + @WithMockUser("admin") + void shouldGetMetadataSuccessfully() throws Exception { + mockMvc + .perform(get(ODATA_BASE_URL + "/$metadata")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/xml")); + } + + // ========== Travels Entity Tests ========== + + @Test + @WithMockUser("admin") + void shouldGetAllTravels() throws Exception { + mockMvc + .perform(get(TRAVELS_ENDPOINT)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.@context", containsString("Travels"))) + .andExpect(jsonPath("$.value").isArray()); + } + + @Test + @WithMockUser("admin") + void shouldCreateTravel() throws Exception { + Travels travel = createTravelData("shouldCreateTravel"); + + mockMvc + .perform(post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson())) + .andExpect(status().isCreated()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Description").value(travel.getDescription())) + .andExpect(jsonPath("$.BeginDate").value(travel.getBeginDate().toString())) + .andExpect(jsonPath("$.EndDate").value(travel.getEndDate().toString())) + .andExpect(jsonPath("$.BookingFee").value(travel.getBookingFee().intValue())) + .andExpect(jsonPath("$.Currency_code").value(travel.getCurrencyCode())) + .andExpect(jsonPath("$.ID", notNullValue())); + } + + @Test + @WithMockUser("admin") + void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { + Travels travel = createTravelData("shouldCreateAndRetrieveTravelSuccessfully"); + travel.setBookingFee(BigDecimal.valueOf(200.0)); + travel.setCurrencyCode("USD"); + + // Create travel + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson())) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify the created travel can be retrieved + ObjectMapper mapper = new ObjectMapper(); + Map createdTravel = + mapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + assertNotNull(travelId); + + mockMvc + .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.BookingFee").value(200.0)) + .andExpect(jsonPath("$.Currency_code").value("USD")); + } + + @Test + @WithMockUser("admin") + void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { + // Test Flights entity + mockMvc + .perform(get(ODATA_BASE_URL + "/Flights")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + + // Test Supplements entity + mockMvc + .perform(get(ODATA_BASE_URL + "/Supplements")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + + // Test Currencies entity + mockMvc + .perform(get(ODATA_BASE_URL + "/Currencies")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")); + } + + @Test + @WithMockUser("admin") + void shouldReturn400ForInvalidDiscountPercentage() throws Exception { + // First create a travel + Travels travelData = createTravelData("shouldReturn400ForInvalidDiscountPercentage"); + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson())) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + ObjectMapper mapper = new ObjectMapper(); + Map createdTravel = + mapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Try to execute deductDiscount action with invalid percentage (>100) + CdsData actionParams = CdsData.create(); + actionParams.put("percent", 150); // Invalid percentage + + mockMvc + .perform( + post(TRAVELS_ENDPOINT + + "(ID=" + + travelId + + ",IsActiveEntity=true)/TravelService.deductDiscount") + .contentType("application/json") + .content(actionParams.toJson())) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser("admin") + void shouldExecuteAcceptTravelAction() throws Exception { + // First create a travel + Travels travelData = createTravelData("shouldExecuteAcceptTravelAction"); + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson())) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + ObjectMapper mapper = new ObjectMapper(); + Map createdTravel = + mapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute acceptTravel action + mockMvc + .perform( + post(TRAVELS_ENDPOINT + + "(ID=" + + travelId + + ",IsActiveEntity=true)/TravelService.acceptTravel") .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) + .content("{}")) + .andExpect(status().is2xxSuccessful()); + + // Check if travel status is accepted + mockMvc + .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Status_code").value("A")); + } + + @Test + @WithMockUser("admin") + void shouldExecuteRejectTravelAction() throws Exception { + // First create a travel + Travels travelData = createTravelData("shouldExecuteRejectTravelAction"); + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson())) .andExpect(status().isCreated()) .andReturn() .getResponse() .getContentAsString(); - // Verify the created travel can be retrieved - Map createdTravel = objectMapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); - assertNotNull(travelId); - - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.BookingFee", is(travelData.get("BookingFee")))) - .andExpect(jsonPath("$.Currency_code", is(travelData.get("Currency_code")))); - } - - @Test - @WithMockUser("admin") - void shouldSupportODataFilterQuery() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/Travels?$filter=Currency_code eq 'EUR'&$top=1&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.value[0].Currency_code", is("EUR"))); - } - - @Test - @WithMockUser("admin") - void shouldSupportSelectQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$select=ID,Description,Currency_code")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value.length()", is(1000))) - .andExpect(jsonPath("$.value[0].length()", is(5))) - .andExpect(jsonPath("$.value[0]", hasKey("ID"))) - .andExpect(jsonPath("$.value[0]", hasKey("Description"))) - .andExpect(jsonPath("$.value[0]", hasKey("Currency_code"))); - } - - @Test - @WithMockUser("admin") - void shouldSupportOrderByQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$orderby=Currency_code desc&$top=3&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value[0].Currency_code", is("USD"))) - .andExpect(jsonPath("$.value[1].Currency_code", is("USD"))) - .andExpect(jsonPath("$.value[2].Currency_code", is("USD"))); - } - - @Test - @WithMockUser("admin") - void shouldSupportTopAndSkipQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=3&$skip=0")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(List.class))) - .andExpect(jsonPath("$.value.length()", is(3))) - .andExpect(jsonPath("$.value[0].ID", is(1))) - .andExpect(jsonPath("$.value[1].ID", is(2))) - .andExpect(jsonPath("$.value[2].ID", is(3))); - - mockMvc.perform(get(TRAVELS_ENDPOINT + "?$top=2&$skip=1")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json;charset=UTF-8")) - .andExpect(jsonPath("$.value", isA(List.class))) - .andExpect(jsonPath("$.value.length()", is(2))) - .andExpect(jsonPath("$.value[0].ID", is(2))) - .andExpect(jsonPath("$.value[1].ID", is(3))); - } - - @Test - @WithMockUser("admin") - void shouldSupportCountQuery() throws Exception { - mockMvc.perform(get(TRAVELS_ENDPOINT + "/$count")) - .andExpect(status().isOk()) - .andExpect(content().string(matchesRegex("\\d+"))); - } - - @Test - @WithMockUser("admin") - void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { - // Test Flights entity - mockMvc.perform(get(ODATA_BASE_URL + "/Flights")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); - - // Test Supplements entity - mockMvc.perform(get(ODATA_BASE_URL + "/Supplements")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); - - // Test Currencies entity - mockMvc.perform(get(ODATA_BASE_URL + "/Currencies")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")); - } - - @Test - @WithMockUser("admin") - void shouldReturn404ForNonExistentEntity() throws Exception { - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID=9999999,IsActiveEntity=false)")) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser("admin") - void shouldReturn400ForInvalidDiscountPercentage() throws Exception { - // First create a travel - Map travelData = createTravelData("shouldReturn400ForInvalidDiscountPercentage"); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Try to execute deductDiscount action with invalid percentage (>100) - Map actionParams = new HashMap<>(); - actionParams.put("percent", 150); // Invalid percentage - - mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.deductDiscount") - .contentType("application/json") - .content(objectMapper.writeValueAsString(actionParams))) - .andExpect(status().isBadRequest()); - } - - @Test - @WithMockUser("admin") - void shouldDeleteTravel() throws Exception { - // First create a travel - Map travelData = createTravelData("shouldDeleteTravel"); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Delete the travel - mockMvc.perform(delete(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) - .andExpect(status().isNoContent()); - - // Verify it's deleted - mockMvc.perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) - .andExpect(status().isNotFound()); - } - - - @Test - @WithMockUser("admin") - void shouldExecuteAcceptTravelAction() throws Exception { - // First create a travel - Map travelData = createTravelData("shouldExecuteAcceptTravelAction"); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Execute acceptTravel action - mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.acceptTravel") - .contentType("application/json") - .content("{}")) - .andExpect(status().is2xxSuccessful()); - - // Check if travel status is accepted - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.Status_code").value("A")); - } - - @Test - @WithMockUser("admin") - void shouldExecuteRejectTravelAction() throws Exception { - // First create a travel - Map travelData = createTravelData("shouldExecuteRejectTravelAction"); - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - - // Execute rejectTravel action - mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.rejectTravel") - .contentType("application/json") - .content("{}")) - .andExpect(status().is2xxSuccessful()); - - // Check if travel status is rejected - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.Status_code").value("X")); - } - - @Test - @WithMockUser("admin") - void shouldExecuteDeductDiscountAction() throws Exception { - // First create a travel - Map travelData = createTravelData("shouldExecuteDeductDiscountAction"); - - String response = mockMvc.perform(post(TRAVELS_ENDPOINT) - .contentType("application/json") - .content(objectMapper.writeValueAsString(travelData))) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getContentAsString(); - - Map createdTravel = objectMapper.readValue(response, Map.class); - Integer travelId = (Integer) createdTravel.get("ID"); - Integer deduction = 10; - - // Execute deductDiscount action with 10% discount - Map actionParams = new HashMap<>(); - actionParams.put("percent", deduction); - - mockMvc.perform(post(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)/TravelService.deductDiscount") - .contentType("application/json") - .content(objectMapper.writeValueAsString(actionParams))) - .andExpect(status().isOk()); - - mockMvc.perform(get(ODATA_BASE_URL + "/Travels(ID="+travelId+",IsActiveEntity=true)")) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("application/json")) - .andExpect(jsonPath("$.BookingFee").value(90)); - } - - private Map createTravelData(String testName) { - synchronized (TravelServiceIntegrationTest.class) { - testCounter++; - } - - Map travelData = new HashMap<>(); - travelData.put("IsActiveEntity", true); - travelData.put("Description", testName + " - Test Travel " + testCounter + " to Paris"); - travelData.put("BeginDate", LocalDate.now().plusDays(30 + testCounter).toString()); - travelData.put("EndDate", LocalDate.now().plusDays(37 + testCounter).toString()); - travelData.put("BookingFee", 100); - travelData.put("Currency_code", "EUR"); - travelData.put("Agency_ID", "070001"); - travelData.put("Customer_ID", "000001"); - return travelData; - } + ObjectMapper mapper = new ObjectMapper(); + Map createdTravel = + mapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute rejectTravel action + mockMvc + .perform( + post(TRAVELS_ENDPOINT + + "(ID=" + + travelId + + ",IsActiveEntity=true)/TravelService.rejectTravel") + .contentType("application/json") + .content("{}")) + .andExpect(status().is2xxSuccessful()); + + // Check if travel status is rejected + mockMvc + .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Status_code").value("X")); + } + + @Test + @WithMockUser("admin") + void shouldExecuteDeductDiscountAction() throws Exception { + // First create a travel + Travels travelData = createTravelData("shouldExecuteDeductDiscountAction"); + + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson())) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + ObjectMapper mapper = new ObjectMapper(); + Map createdTravel = + mapper.readValue(response, new TypeReference>() {}); + Integer travelId = (Integer) createdTravel.get("ID"); + + // Execute deductDiscount action with 10% discount + CdsData actionParams = CdsData.create(); + actionParams.put("percent", 10); + + mockMvc + .perform( + post(TRAVELS_ENDPOINT + + "(ID=" + + travelId + + ",IsActiveEntity=true)/TravelService.deductDiscount") + .contentType("application/json") + .content(actionParams.toJson())) + .andExpect(status().isOk()); + + mockMvc + .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.BookingFee").value(90)); + } + + private Travels createTravelData(String testName) { + Travels travel = Travels.create(); + travel.setIsActiveEntity(true); + travel.setDescription(testName + " - Test Travel"); + travel.setBeginDate(LocalDate.now().plusDays(30)); + travel.setEndDate(LocalDate.now().plusDays(37)); + travel.setBookingFee(BigDecimal.valueOf(100)); + travel.setCurrencyCode("EUR"); + travel.setAgencyId("070001"); + travel.setCustomerId("000001"); + return travel; + } } diff --git a/test/http/TravelService.http b/test/http/TravelService.http deleted file mode 100644 index 07dc885..0000000 --- a/test/http/TravelService.http +++ /dev/null @@ -1,275 +0,0 @@ -@server=http://localhost:8080 -@username=admin -@password= - - -### Travels -# @name Travels_GET -GET {{server}}/odata/v4/travel/Travels -Authorization: Basic {{username}}:{{password}} - - -### Travels Drafts GET -# @name Travels_Drafts_GET -GET {{server}}/odata/v4/travel/Travels?$filter=(IsActiveEntity eq false) -Authorization: Basic {{username}}:{{password}} - -### Travels Drafts GET -# @name Travels_Drafts_GET -GET http://localhost:8080/odata/v4/travel/Travels(ID=1,IsActiveEntity=true) -Authorization: Basic {{username}}:{{password}} - - - -### Travels Draft POST -# @name Travels_Draft_POST -POST {{server}}/odata/v4/travel/Travels -Content-Type: application/json -Authorization: Basic {{username}}:{{password}} - -{ - "Description": "Description-23174596", - "BeginDate": "2018-07-31", - "EndDate": "2016-08-01", - "BookingFee": 90204.5092, - "TotalPrice": 81479.652, - "Currency": { - "code": "890" - }, - "Status": { - "code": "A" - }, - "Agency": { - "ID": "270904" - }, - "Customer": { - "ID": "588238" - }, - "Bookings": [ - { - "Pos": 16001352, - "Flight": { - "ID": "ts-20071832", - "date": "2004-01-03" - }, - "FlightPrice": 69100.9987, - "Currency": { - "code": "890" - }, - "Supplements": [ - { - "ID": "10036670-2fb0-4a00-89e0-c433979ff5b9", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 63025.421, - "Currency": { - "code": "890" - } - }, - { - "ID": "10036681-bc8d-4063-ae7e-2a7e2d222a49", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 50718.3963, - "Currency": { - "code": "890" - } - } - ], - "BookingDate": "2003-08-28" - }, - { - "Pos": 16001353, - "Flight": { - "ID": "ts-20071832", - "date": "2004-01-03" - }, - "FlightPrice": 49406.9219, - "Currency": { - "code": "890" - }, - "Supplements": [ - { - "ID": "18472766-b0be-456b-808f-3525c58cdb7a", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 81274.8795, - "Currency": { - "code": "890" - } - }, - { - "ID": "18472767-7033-4186-9ae4-eaa8d3d16780", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 40101.901, - "Currency": { - "code": "890" - } - } - ], - "BookingDate": "2018-04-23" - } - ], - "createdAt": "2012-02-16T23:00:00.000Z", - "createdBy": "createdBy.dspms@example.net", - "modifiedAt": "2023-12-29T23:00:00.000Z", - "modifiedBy": "modifiedBy.dspms@example.org" -} - - -### Result from POST request above -@draftID={{Travels_Draft_POST.response.body.$.ID}} - - -### Travels Draft PATCH -# @name Travels_Draft_Patch -PATCH {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false) -Content-Type: application/json -Authorization: Basic {{username}}:{{password}} - -{ - "Description": "Description-23174596", - "BeginDate": "2018-07-31", - "EndDate": "2016-08-01", - "BookingFee": 90204.5092, - "TotalPrice": 81479.652, - "Currency": { - "code": "890" - }, - "Status": { - "code": "A" - }, - "Agency": { - "ID": "270904" - }, - "Customer": { - "ID": "588238" - }, - "Bookings": [ - { - "Pos": 16001352, - "Flight": { - "ID": "ts-20071832", - "date": "2004-01-03" - }, - "FlightPrice": 69100.9987, - "Currency": { - "code": "890" - }, - "Supplements": [ - { - "ID": "10036670-2fb0-4a00-89e0-c433979ff5b9", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 63025.421, - "Currency": { - "code": "890" - } - }, - { - "ID": "10036681-bc8d-4063-ae7e-2a7e2d222a49", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 50718.3963, - "Currency": { - "code": "890" - } - } - ], - "BookingDate": "2003-08-28" - }, - { - "Pos": 16001353, - "Flight": { - "ID": "ts-20071832", - "date": "2004-01-03" - }, - "FlightPrice": 49406.9219, - "Currency": { - "code": "890" - }, - "Supplements": [ - { - "ID": "18472766-b0be-456b-808f-3525c58cdb7a", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 81274.8795, - "Currency": { - "code": "890" - } - }, - { - "ID": "18472767-7033-4186-9ae4-eaa8d3d16780", - "booked": { - "ID": "30007216-09c4-4fe0-8b54-feebc9fd7a0c" - }, - "Price": 40101.901, - "Currency": { - "code": "890" - } - } - ], - "BookingDate": "2018-04-23" - } - ], - "createdAt": "2012-02-16T23:00:00.000Z", - "createdBy": "createdBy.dspms@example.net", - "modifiedAt": "2023-12-29T23:00:00.000Z", - "modifiedBy": "modifiedBy.dspms@example.org" -} - - -### Travels Draft Prepare -# @name Travels_Draft_Prepare -POST {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false)/TravelService.draftPrepare -Content-Type: application/json -Authorization: Basic {{username}}:{{password}} - -{} - - -### Travels Draft Activate -# @name Travels_Draft_Activate -POST {{server}}/odata/v4/travel/Travels(ID={{draftID}},IsActiveEntity=false)/TravelService.draftActivate -Content-Type: application/json -Authorization: Basic {{username}}:{{password}} - -{} - - -### Currencies -# @name Currencies_GET -GET {{server}}/odata/v4/travel/Currencies -Authorization: Basic {{username}}:{{password}} - - -### TravelStatus -# @name TravelStatus_GET -GET {{server}}/odata/v4/travel/TravelStatus -Authorization: Basic {{username}}:{{password}} - - -### Flights -# @name Flights_GET -GET {{server}}/odata/v4/travel/Flights -Authorization: Basic {{username}}:{{password}} - - -### Supplements -# @name Supplements_GET -GET {{server}}/odata/v4/travel/Supplements -Authorization: Basic {{username}}:{{password}} - - -### SupplementTypes -# @name SupplementTypes_GET -GET {{server}}/odata/v4/travel/SupplementTypes -Authorization: Basic {{username}}:{{password}} diff --git a/test/http/sap.capire.flights.data.http b/test/http/sap.capire.flights.data.http deleted file mode 100644 index 4a84f98..0000000 --- a/test/http/sap.capire.flights.data.http +++ /dev/null @@ -1,33 +0,0 @@ -@server=http://localhost:8080 -@username=admin -@password= - - -### Flights -# @name Flights_GET -GET {{server}}/odata/v4/sap.capire.flights.data/Flights -Authorization: Basic {{username}}:{{password}} - - -### Airlines -# @name Airlines_GET -GET {{server}}/odata/v4/sap.capire.flights.data/Airlines -Authorization: Basic {{username}}:{{password}} - - -### Airports -# @name Airports_GET -GET {{server}}/odata/v4/sap.capire.flights.data/Airports -Authorization: Basic {{username}}:{{password}} - - -### Supplements -# @name Supplements_GET -GET {{server}}/odata/v4/sap.capire.flights.data/Supplements -Authorization: Basic {{username}}:{{password}} - - -### SupplementTypes -# @name SupplementTypes_GET -GET {{server}}/odata/v4/sap.capire.flights.data/SupplementTypes -Authorization: Basic {{username}}:{{password}} From 179cd3e1872539aeaf5a28c0f52d9e50bd3ad0d0 Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Wed, 7 Jan 2026 14:46:06 +0100 Subject: [PATCH 29/30] Add test with bookings, flights and supplements --- app/travels/field-control.cds | 2 +- .../xtravels/handler/CreationHandler.java | 12 +- .../TravelServiceIntegrationTest.java | 108 ++++++++++++------ 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/app/travels/field-control.cds b/app/travels/field-control.cds index b50de98..cabc02f 100644 --- a/app/travels/field-control.cds +++ b/app/travels/field-control.cds @@ -29,7 +29,7 @@ annotate TravelService.Bookings with @UI.CreateHidden : (Travel.Status.code != # annotate TravelService.Bookings with @UI.DeleteHidden : (Travel.Status.code != #Open); annotate TravelService.Bookings { - BookingDate @Core.Computed; + BookingDate @readonly; Flight @readonly: (Travel.Status.code = #Accepted) @mandatory: (Travel.Status.code != #Accepted); FlightPrice @readonly: (Travel.Status.code = #Accepted) @mandatory: (Travel.Status.code != #Accepted); }; diff --git a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java index bc97dce..01a50aa 100644 --- a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java +++ b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java @@ -13,6 +13,7 @@ import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.Before; import com.sap.cds.services.handler.annotations.ServiceName; +import java.time.LocalDate; import org.springframework.stereotype.Component; @Component @@ -37,10 +38,18 @@ void calculateTravelId(final Travels travel) { .columns(t -> t.ID().max().as("maxID"))); int maxId = (int) result.single().get("maxID"); travel.setId(++maxId); + + if (travel.getBookings() != null) { + int nextPos = 1; + for (Bookings booking : travel.getBookings()) { + booking.setPos(nextPos++); + booking.setBookingDate(LocalDate.now()); // $now uses timestamp unexpectedly + } + } } // Fill in IDs as sequence numbers -> could be automated by auto-generation - @Before(event = EVENT_DRAFT_NEW) + @Before(event = {EVENT_CREATE, EVENT_DRAFT_NEW}) void calculateBookingPos(Bookings_ ref, final Bookings booking) { var result = service.run(Select.from(ref).columns(t -> t.Pos().max().as("maxPos"))); var maxPos = result.single().get("maxPos"); @@ -50,5 +59,6 @@ void calculateBookingPos(Bookings_ ref, final Bookings booking) { int pos = (int) maxPos; booking.setPos(++pos); } + booking.setBookingDate(LocalDate.now()); } } diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 8b7a06c..60b22c7 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -9,13 +9,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import cds.gen.travelservice.Bookings; import cds.gen.travelservice.Travels; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.CdsData; +import com.sap.cds.CdsJsonConverter; +import com.sap.cds.CdsJsonConverter.UnknownPropertyHandling; +import com.sap.cds.reflect.CdsModel; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Map; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -32,6 +35,16 @@ class TravelServiceIntegrationTest { private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels"; @Autowired private MockMvc mockMvc; + @Autowired private CdsModel model; + private CdsJsonConverter converter; + + @BeforeEach + void setup() { + converter = + CdsJsonConverter.builder(model) + .unknownPropertyHandling(UnknownPropertyHandling.IGNORE) + .build(); + } @Test @WithMockUser("admin") @@ -90,20 +103,59 @@ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception { .getContentAsString(); // Verify the created travel can be retrieved - ObjectMapper mapper = new ObjectMapper(); - Map createdTravel = - mapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); - assertNotNull(travelId); + Travels createdTravel = converter.fromJsonObject(response, Travels.class); + assertNotNull(createdTravel.getId()); mockMvc - .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")) .andExpect(jsonPath("$.BookingFee").value(200.0)) .andExpect(jsonPath("$.Currency_code").value("USD")); } + @Test + @WithMockUser("admin") + void shouldCreateAndRetrieveTravelWithBookingsSuccessfully() throws Exception { + Travels travel = createTravelData("shouldCreateAndRetrieveTravelWithBookingsSuccessfully"); + Bookings booking = Bookings.create(); + booking.setFlightId("GA0322"); + booking.setFlightDate(LocalDate.of(2024, 6, 2)); + booking.setFlightPrice(BigDecimal.valueOf(1103)); + Bookings.Supplements supplement = Bookings.Supplements.create(); + supplement.setBookedId("bv-0001"); + supplement.setPrice(new BigDecimal("2.30")); + booking.setSupplements(List.of(supplement)); + travel.setBookings(List.of(booking)); + + // Create travel + String response = + mockMvc + .perform( + post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson())) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify the created travel can be retrieved + Travels createdTravel = converter.fromJsonObject(response, Travels.class); + assertNotNull(createdTravel.getId()); + + // Verify @federated data can be read + mockMvc + .perform( + get( + TRAVELS_ENDPOINT + + "(ID=" + + createdTravel.getId() + + ",IsActiveEntity=true)?$expand=Bookings($expand=Flight,Supplements($expand=booked))")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("application/json")) + .andExpect(jsonPath("$.Bookings[0].Flight.origin").value("Miami International Airport")) + .andExpect(jsonPath("$.Bookings[0].Supplements[0].booked.descr").value("Hot Chocolate")); + } + @Test @WithMockUser("admin") void shouldGetReadOnlyEntitiesSuccessfully() throws Exception { @@ -140,10 +192,7 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { .getResponse() .getContentAsString(); - ObjectMapper mapper = new ObjectMapper(); - Map createdTravel = - mapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); + Travels createdTravel = converter.fromJsonObject(response, Travels.class); // Try to execute deductDiscount action with invalid percentage (>100) CdsData actionParams = CdsData.create(); @@ -153,7 +202,7 @@ void shouldReturn400ForInvalidDiscountPercentage() throws Exception { .perform( post(TRAVELS_ENDPOINT + "(ID=" - + travelId + + createdTravel.getId() + ",IsActiveEntity=true)/TravelService.deductDiscount") .contentType("application/json") .content(actionParams.toJson())) @@ -174,17 +223,14 @@ void shouldExecuteAcceptTravelAction() throws Exception { .getResponse() .getContentAsString(); - ObjectMapper mapper = new ObjectMapper(); - Map createdTravel = - mapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); + Travels createdTravel = converter.fromJsonObject(response, Travels.class); // Execute acceptTravel action mockMvc .perform( post(TRAVELS_ENDPOINT + "(ID=" - + travelId + + createdTravel.getId() + ",IsActiveEntity=true)/TravelService.acceptTravel") .contentType("application/json") .content("{}")) @@ -192,7 +238,7 @@ void shouldExecuteAcceptTravelAction() throws Exception { // Check if travel status is accepted mockMvc - .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")) .andExpect(jsonPath("$.Status_code").value("A")); @@ -212,17 +258,14 @@ void shouldExecuteRejectTravelAction() throws Exception { .getResponse() .getContentAsString(); - ObjectMapper mapper = new ObjectMapper(); - Map createdTravel = - mapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); + Travels createdTravel = converter.fromJsonObject(response, Travels.class); // Execute rejectTravel action mockMvc .perform( post(TRAVELS_ENDPOINT + "(ID=" - + travelId + + createdTravel.getId() + ",IsActiveEntity=true)/TravelService.rejectTravel") .contentType("application/json") .content("{}")) @@ -230,7 +273,7 @@ void shouldExecuteRejectTravelAction() throws Exception { // Check if travel status is rejected mockMvc - .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")) .andExpect(jsonPath("$.Status_code").value("X")); @@ -251,10 +294,7 @@ void shouldExecuteDeductDiscountAction() throws Exception { .getResponse() .getContentAsString(); - ObjectMapper mapper = new ObjectMapper(); - Map createdTravel = - mapper.readValue(response, new TypeReference>() {}); - Integer travelId = (Integer) createdTravel.get("ID"); + Travels createdTravel = converter.fromJsonObject(response, Travels.class); // Execute deductDiscount action with 10% discount CdsData actionParams = CdsData.create(); @@ -264,14 +304,14 @@ void shouldExecuteDeductDiscountAction() throws Exception { .perform( post(TRAVELS_ENDPOINT + "(ID=" - + travelId + + createdTravel.getId() + ",IsActiveEntity=true)/TravelService.deductDiscount") .contentType("application/json") .content(actionParams.toJson())) .andExpect(status().isOk()); mockMvc - .perform(get(TRAVELS_ENDPOINT + "(ID=" + travelId + ",IsActiveEntity=true)")) + .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("application/json")) .andExpect(jsonPath("$.BookingFee").value(90)); @@ -281,8 +321,8 @@ private Travels createTravelData(String testName) { Travels travel = Travels.create(); travel.setIsActiveEntity(true); travel.setDescription(testName + " - Test Travel"); - travel.setBeginDate(LocalDate.now().plusDays(30)); - travel.setEndDate(LocalDate.now().plusDays(37)); + travel.setBeginDate(LocalDate.of(2024, 6, 1)); + travel.setEndDate(LocalDate.of(2024, 6, 14)); travel.setBookingFee(BigDecimal.valueOf(100)); travel.setCurrencyCode("EUR"); travel.setAgencyId("070001"); From a8d49949f2236ba1e750bcd078a27c970baae796 Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Wed, 7 Jan 2026 14:48:54 +0100 Subject: [PATCH 30/30] cosmetics --- .../java/sap/capire/xtravels/TravelServiceIntegrationTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java index 60b22c7..aca2429 100644 --- a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java +++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java @@ -55,8 +55,6 @@ void shouldGetMetadataSuccessfully() throws Exception { .andExpect(content().contentTypeCompatibleWith("application/xml")); } - // ========== Travels Entity Tests ========== - @Test @WithMockUser("admin") void shouldGetAllTravels() throws Exception {