Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9bb6563
first integration teset
rjayasinghe Dec 5, 2025
c6e99f9
extending tests
rjayasinghe Dec 5, 2025
3af259c
more tests
rjayasinghe Dec 5, 2025
1fa103c
WIP
rjayasinghe Dec 8, 2025
81c049e
revert AI change in production code
rjayasinghe Dec 8, 2025
1ec21e9
fix tests
rjayasinghe Dec 8, 2025
f3403d1
cleanup test
rjayasinghe Dec 8, 2025
7ecee24
more simple odata tests
rjayasinghe Dec 8, 2025
fea99a3
Merge branch 'main' into tests
rjayasinghe Dec 8, 2025
34cad1f
add bound action test
rjayasinghe Dec 8, 2025
3364823
actually assert travel accept action result
rjayasinghe Dec 9, 2025
2114d75
added http files
rjayasinghe Dec 9, 2025
f8e24af
add recalculate price logic for travels without bookings
rjayasinghe Dec 9, 2025
323250b
add more tests for actions
rjayasinghe Dec 9, 2025
e1e28ae
Update srv/src/main/java/sap/capire/xtravels/handler/RecalculatePrice…
rjayasinghe Dec 9, 2025
f6bee47
Update test/http/TravelService.http
rjayasinghe Dec 9, 2025
cf183a8
assert currency_code and booking fee after creation
rjayasinghe Dec 9, 2025
3ee53ca
asssert that currency code is EUR
rjayasinghe Dec 9, 2025
a203868
assert the ordering of the result
rjayasinghe Dec 9, 2025
6a910a9
assert size of results
rjayasinghe Dec 9, 2025
998a2fa
use more stable order criteria
rjayasinghe Dec 9, 2025
165750f
Improved test expectations
davidhunglam Dec 10, 2025
7c49875
Merge branch 'main' into tests
beckermarc Dec 15, 2025
aafd98c
Merge branch 'main' into tests
davidhunglam Dec 15, 2025
887dec8
Added test scope
davidhunglam Dec 16, 2025
eebfa3b
Removing handler code
davidhunglam Dec 16, 2025
97aa9dd
Import order
davidhunglam Dec 16, 2025
a309a15
Import order
davidhunglam Dec 16, 2025
9674071
cds-services 4.6.0
davidhunglam Dec 17, 2025
96972d3
Dealing with that TotalPrice could be null
davidhunglam Dec 17, 2025
d4f4eee
Merge branch 'main' into tests
beckermarc Jan 7, 2026
6c56b59
Simplify
beckermarc Jan 7, 2026
179cd3e
Add test with bookings, flights and supplements
beckermarc Jan 7, 2026
a8d4994
cosmetics
beckermarc Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/travels/field-control.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
2 changes: 1 addition & 1 deletion db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions srv/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
Expand All @@ -50,5 +59,6 @@ void calculateBookingPos(Bookings_ ref, final Bookings booking) {
int pos = (int) maxPos;
booking.setPos(++pos);
}
booking.setBookingDate(LocalDate.now());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
package sap.capire.xtravels;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue;
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;

import cds.gen.travelservice.Bookings;
import cds.gen.travelservice.Travels;
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.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;
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 */
@SpringBootTest
@AutoConfigureMockMvc
class TravelServiceIntegrationTest {

private static final String ODATA_BASE_URL = "/odata/v4/travel";
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")
void shouldGetMetadataSuccessfully() throws Exception {
mockMvc
.perform(get(ODATA_BASE_URL + "/$metadata"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("application/xml"));
}

@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
Travels createdTravel = converter.fromJsonObject(response, Travels.class);
assertNotNull(createdTravel.getId());

mockMvc
.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 {
// 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();

Travels createdTravel = converter.fromJsonObject(response, Travels.class);

// 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="
+ createdTravel.getId()
+ ",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();

Travels createdTravel = converter.fromJsonObject(response, Travels.class);

// Execute acceptTravel action
mockMvc
.perform(
post(TRAVELS_ENDPOINT
+ "(ID="
+ createdTravel.getId()
+ ",IsActiveEntity=true)/TravelService.acceptTravel")
.contentType("application/json")
.content("{}"))
.andExpect(status().is2xxSuccessful());

// Check if travel status is accepted
mockMvc
.perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",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();

Travels createdTravel = converter.fromJsonObject(response, Travels.class);

// Execute rejectTravel action
mockMvc
.perform(
post(TRAVELS_ENDPOINT
+ "(ID="
+ createdTravel.getId()
+ ",IsActiveEntity=true)/TravelService.rejectTravel")
.contentType("application/json")
.content("{}"))
.andExpect(status().is2xxSuccessful());

// Check if travel status is rejected
mockMvc
.perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",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();

Travels createdTravel = converter.fromJsonObject(response, Travels.class);

// Execute deductDiscount action with 10% discount
CdsData actionParams = CdsData.create();
actionParams.put("percent", 10);

mockMvc
.perform(
post(TRAVELS_ENDPOINT
+ "(ID="
+ createdTravel.getId()
+ ",IsActiveEntity=true)/TravelService.deductDiscount")
.contentType("application/json")
.content(actionParams.toJson()))
.andExpect(status().isOk());

mockMvc
.perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",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.of(2024, 6, 1));
travel.setEndDate(LocalDate.of(2024, 6, 14));
travel.setBookingFee(BigDecimal.valueOf(100));
travel.setCurrencyCode("EUR");
travel.setAgencyId("070001");
travel.setCustomerId("000001");
return travel;
}
}