Compare commits

..

7 Commits

Author SHA1 Message Date
Marc Becker
7bb08b2e59 Adapt checkpoint to exercise update 2020-02-14 15:20:55 +00:00
indu sankar
a0ca06fe45 Testcases for products-service,bookstore in java (#11)
* Testcases for products-service,bookstore
* Put back 'sap.capire.bookstore-Books_texts.csv' file, ignored it in DataLoader.
2019-12-13 09:39:48 +01:00
Marc Becker
1c18ea5105 Set CDS version to 3.17.4 in bookstore 2019-10-02 10:13:13 +00:00
Marc Becker
1c79c2e4f2 Set CDS version to 3.17.4 in products-service 2019-10-02 10:11:57 +00:00
Adrian Görler
c124137ab7 Use SQL timestamp literal format
Only the SQL timestamp literal format works with SQLite on JavaScript and Java
as well as with HANA.
2019-09-23 16:14:13 +02:00
Adrian Görler
4838de7f71 Update sap.capire.bookstore-Books.csv 2019-09-23 14:38:22 +02:00
Marc Becker
d631b5106e CAA160 final state 2019-09-18 07:50:21 +00:00
11 changed files with 565 additions and 1 deletions

7
bookstore/manifest.yml Normal file
View File

@@ -0,0 +1,7 @@
---
applications:
- name: bookstore
path: srv/target/bookstore-1.0-SNAPSHOT.jar
random-route: true
services:
- bookstore-hana

View File

@@ -11,9 +11,11 @@
},
"dependencies": {
"@sap/cds": "3.21.2",
"@sap/capire-products": "file:sap-capire-products-1.0.0.tgz"
"@sap/capire-products": "file:sap-capire-products-1.0.0.tgz",
"hdb": "^0.17.1"
},
"devDependencies": {
"@sap/hdi-deploy": "3.7.0",
"sqlite3": "^4.1.1"
},
"cds": {

View File

@@ -21,10 +21,25 @@
<artifactId>cds-starter-spring-boot-odata</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-hana</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-cloudfoundry</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,105 @@
package com.sap.teched.cap.bookstore.handlers;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.CdsService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.persistence.PersistenceService;
import cds.gen.ordersservice.OrderItems;
import cds.gen.ordersservice.Orders;
import cds.gen.sap.capire.bookstore.Books;
import cds.gen.sap.capire.bookstore.Books_;
import cds.gen.sap.capire.bookstore.OrderItems_;
@Component
@ServiceName("OrdersService")
public class OrdersService implements EventHandler {
@Autowired
PersistenceService db;
@Before(event = CdsService.EVENT_CREATE, entity = "OrdersService.OrderItems")
public void validateBookAndDecreaseStock(List<OrderItems> items) {
for (OrderItems item : items) {
String bookId = item.getBookId();
Integer amount = item.getAmount();
// check if the book that should be ordered is existing
CqnSelect sel = Select.from(Books_.class).columns(b -> b.stock()).where(b -> b.ID().eq(bookId));
Books book = db.run(sel).first(Books.class).orElseThrow(() -> new ServiceException(ErrorStatuses.NOT_FOUND, "Book does not exist"));
// check if order could be fulfilled
int stock = book.getStock();
if (stock < amount) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Not enough books on stock");
}
// update the book with the new stock, means minus the order amount
book.setStock(stock - amount);
CqnUpdate update = Update.entity(Books_.class).data(book).where(b -> b.ID().eq(bookId));
db.run(update);
}
}
@Before(event = CdsService.EVENT_CREATE, entity = "OrdersService.Orders")
public void validateBookAndDecreaseStockViaOrders(List<Orders> orders) {
for(Orders order : orders) {
if(order.getItems() != null) {
validateBookAndDecreaseStock(order.getItems());
}
}
}
@After(event = { CdsService.EVENT_READ, CdsService.EVENT_CREATE }, entity = "OrdersService.OrderItems")
public void calculateNetAmount(List<OrderItems> items) {
for (OrderItems item : items) {
String bookId = item.getBookId();
// get the book that was ordered
CqnSelect sel = Select.from(Books_.class).where(b -> b.ID().eq(bookId));
Books book = db.run(sel).single(Books.class);
// calculate and set net amount
item.setNetAmount(book.getPrice().multiply(new BigDecimal(item.getAmount())));
}
}
@After(event = { CdsService.EVENT_READ, CdsService.EVENT_CREATE }, entity = "OrdersService.Orders")
public void calculateTotal(List<Orders> orders) {
for (Orders order : orders) {
// calculate net amount for expanded items
if(order.getItems() != null) {
calculateNetAmount(order.getItems());
}
// get all items of the order
CqnSelect selItems = Select.from(OrderItems_.class).where(i -> i.parent().ID().eq(order.getId()));
List<OrderItems> allItems = db.run(selItems).listOf(OrderItems.class);
// calculate net amount of all items
calculateNetAmount(allItems);
// calculate and set the orders total
BigDecimal total = new BigDecimal(0);
for(OrderItems item : allItems) {
total = total.add(item.getNetAmount());
}
order.setTotal(total);
}
}
}

View File

@@ -0,0 +1,165 @@
package com.sap.teched.cap.bookstore.handlers;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
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.request.MockMvcRequestBuilders.put;
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 java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import cds.gen.sap.capire.bookstore.OrderItems;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class orderServiceTest {
private static final String ODATA_V4 = "/odata/v4";
private static final String BOOKS_URI = ODATA_V4 + "/BooksService/Books";
private static final String ORDERS_URI = ODATA_V4 + "/OrdersService/Orders";
private static final String ORDER_ITEMS_URI = ODATA_V4 + "/OrdersService/OrderItems";
@Autowired
private MockMvc mockMvc;
@Test
public void testGetBooks() throws Exception {
mockMvc.perform(get(BOOKS_URI).content("")).andExpect(status().isOk())
.andExpect(jsonPath("$.value[0].title", is("Wuthering Heights")));
}
@Test
public void testCreateOrderItem() throws Exception {
final String data = "{\"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\"book_ID\": \"abed2f7a-c50e-4bc5-89fd-9a00a54b4b16\", \"amount\": 1 }";
mockMvc.perform(post(ORDER_ITEMS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(get(ORDER_ITEMS_URI).content("")).andExpect(status().isOk())
.andExpect(jsonPath("$.value[0].parent_ID", is("12345678-aaaa-bbbb-dddd-ddddeeeeffef")));
}
@Test
public void tesValidateBookAndDecreaseStock() throws Exception {
final String data = "{\"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffee\",\"book_ID\": \"5e69a718-d86b-4461-8953-0edadcd87960\", \"amount\": 20 }";
mockMvc.perform(post(ORDER_ITEMS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(post(ORDER_ITEMS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isBadRequest());
}
@Test
public void tesCalculateNetAmount() throws Exception {
final String data = "{\"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffbb\",\"book_ID\": \"b7bca6dd-0497-465e-9a5a-56f244174c8c\", \"amount\": 10 }";
MvcResult result = mockMvc.perform(post(ORDER_ITEMS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andReturn();
JSONObject jsonObj = new JSONObject(result.getResponse().getContentAsString());
String id = jsonObj.get("ID").toString();
mockMvc.perform(get(ORDER_ITEMS_URI+ "(" + id + ")").content("")).andExpect(status().isOk())
.andExpect(jsonPath("$.netAmount", is(141.4)));
}
@Test
public void testCreateOrder() throws Exception {
final String orderData = "{\r\n" +
" \"items\": [{\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"abed2f7a-c50e-4bc5-89fd-9a00a54b4b16\",\r\n" +
" \"amount\": 1\r\n" +
" }, {\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"b7bca6dd-0497-465e-9a5a-56f244174c8c\",\r\n" +
" \"amount\": 1\r\n" +
" }]\r\n" +
"}";
mockMvc.perform(post(ORDERS_URI).contentType(APPLICATION_JSON_UTF8).content(
orderData)) .andExpect(status().isCreated());
}
@Test
public void testCalculateTotal() throws Exception {
final String orderData = "{\r\n" +
" \"items\": [{\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"abed2f7a-c50e-4bc5-89fd-9a00a54b4b16\",\r\n" +
" \"amount\": 1\r\n" +
" }, {\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"b7bca6dd-0497-465e-9a5a-56f244174c8c\",\r\n" +
" \"amount\": 2\r\n" +
" }]\r\n" +
"}";
MvcResult result = mockMvc.perform(post(ORDERS_URI).contentType(APPLICATION_JSON_UTF8).content(orderData))
.andReturn();
JSONObject jsonObj = new JSONObject(result.getResponse().getContentAsString());
String id = jsonObj.get("ID").toString();
mockMvc.perform(get(ORDERS_URI+ "(" + id + ")").content("")).andExpect(status().isOk())
.andExpect(jsonPath("$.total", is(39.39)));
}
@Test
public void testValidateBookAndDecreaseStockViaOrders() throws Exception {
final String orderData = "{\r\n" +
" \"items\": [{\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"fd0c5fda-8811-4e20-bcff-3a776abc290a\",\r\n" +
" \"amount\": 300\r\n" +
" }, {\r\n" +
" \"parent_ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\",\r\n" +
" \"book_ID\": \"b7bca6dd-0497-465e-9a5a-56f244174c8c\",\r\n" +
" \"amount\": 1\r\n" +
" }]\r\n" +
"}";
mockMvc.perform(post(ORDERS_URI).contentType(APPLICATION_JSON_UTF8).content(orderData))
.andExpect(status().isCreated());
mockMvc.perform(post(ORDERS_URI).contentType(APPLICATION_JSON_UTF8).content(orderData))
.andExpect(status().isBadRequest());
}
}

View File

@@ -0,0 +1,82 @@
package com.sap.teched.cap.bookstore.testUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.sap.cds.ql.Insert;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsSimpleType;
import com.sap.cds.services.persistence.PersistenceService;
public class CsvUploader {
private final CdsModel model;
private final PersistenceService ds;
public CsvUploader(CdsModel model, PersistenceService ds) {
this.model = model;
this.ds = ds;
}
void insertContent(File file) throws FileNotFoundException, IOException {
String fileName = file.getName();
String entityName = fileName.replace("-", ".").substring(0, fileName.length() - 4);
CdsEntity entity = model.getEntity(entityName);
List<Map<String, Object>> data = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line = br.readLine();
CdsElement[] headers = getHeaders(entity, line);
while ((line = br.readLine()) != null) {
Map<String, Object> row = new HashMap<>();
String[] values = line.split(";");
for (int i = 0; i < headers.length; i++) {
CdsElement element = headers[i];
String typeName = element.getType().as(CdsSimpleType.class).getQualifiedName();
final Object value;
String txt = values[i];
if ("".equals(txt)) {
continue;
}
switch (typeName) {
case "cds.Integer":
value = Integer.valueOf(txt);
break;
case "cds.Date":
value = LocalDate.parse(txt);
break;
default:
value = txt;
}
row.put(element.getName(), value);
}
data.add(row);
}
}
Insert insert = Insert.into(entity).entries(data);
ds.run(insert);
}
private CdsElement[] getHeaders(CdsEntity entity, String line) {
String[] elementNames = line.split(";");
CdsElement[] result = new CdsElement[elementNames.length];
for (int i = 0; i < elementNames.length; i++) {
result[i] = entity.getElement(elementNames[i]);
}
return result;
}
}

View File

@@ -0,0 +1,45 @@
package com.sap.teched.cap.bookstore.testUtil;
import java.io.File;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.runtime.CdsRuntime;
@Component
public class DataLoader implements ApplicationRunner {
@Autowired
private CdsRuntime runtime;
public void run(ApplicationArguments args) {
PersistenceService ds = (PersistenceService) runtime.getServiceCatalog()
.getService(PersistenceService.DEFAULT_NAME);
runtime.runInRequestContext(null, r -> {
runtime.runInChangeSetContext(c -> {
File folder = new File("../db/data");
CsvUploader uploader = new CsvUploader(runtime.getCdsModel(), ds);
for (File file : folder.listFiles()) {
try {
if(file.getName().equals("sap.capire.bookstore-Books_texts.csv"))
continue;
uploader.insertContent(file);
} catch (RuntimeException | IOException e) {
throw new RuntimeException("exception reading file " + file.getName(), e);
}
}
return null;
});
return null;
});
}
}

View File

@@ -0,0 +1,7 @@
---
spring:
profiles: default
datasource:
url: "jdbc:sqlite::memory:"
driver-class-name: org.sqlite.JDBC
initialization-mode: always

View File

@@ -25,6 +25,11 @@
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,124 @@
package com.sap.teched.cap.productsservice;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
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.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class productServiceTest {
private static final String ODATA_V4 = "/odata/v4";
private static final String PRODUCTS_URI = ODATA_V4 + "/AdminService/Products";
private static final String CATEGORIES_URI = ODATA_V4 + "/AdminService/Categories";
private static final String CURRENCIES_URI = ODATA_V4 + "/AdminService/Currencies";
@Autowired
private MockMvc mockMvc;
@Test
public void testProducts() throws Exception {
mockMvc.perform(get(PRODUCTS_URI).content("")).andExpect(status().isOk());
}
@Test
public void testPostProducts() throws Exception {
final String data = "{\"ID\": \"12345678-aaaa-bbbb-cccc-ddddeeeeffff\", \"title\": \"Awesome product\", \"descr\": \"It is really really awesome!\"}";
mockMvc.perform(post(PRODUCTS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
}
@Test
public void testGetProduct() throws Exception {
final String id = "12345611-aaaa-bbbb-cccc-ddddeeeeffef";
final String data = "{\"ID\": \"12345611-aaaa-bbbb-cccc-ddddeeeeffef\", \"title\": \"Awesome book\", \"descr\": \"It is an awesome book!\"}";
mockMvc.perform(post(PRODUCTS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(get(PRODUCTS_URI + "(" + id + ")").content("")).andExpect(status().isOk())
.andExpect(jsonPath("$.title", is("Awesome book")));
}
@Test
public void testEditProduct() throws Exception {
final String id = "12345678-aaaa-bbbb-dddd-ddddeeeeffef";
final String data = "{\"ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\", \"title\": \"Awesome book\", \"descr\": \"It is an awesome book!\"}";
final String changedData = "{\"ID\": \"12345678-aaaa-bbbb-dddd-ddddeeeeffef\", \"title\": \"Awesome book\", \"descr\": \"It is a good book! I love it\"}";
mockMvc.perform(post(PRODUCTS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(put(PRODUCTS_URI + "(" + id + ")").contentType(APPLICATION_JSON_UTF8).content(changedData))
.andExpect(status().isOk()).andExpect(jsonPath("$.modifiedAt", is(not(nullValue()))));
}
@Test
public void testDeleteProduct() throws Exception {
final String id = "12345678-aaaa-bbbb-cccc-ddddeeeeffdd";
final String data = "{\"ID\": \"12345678-aaaa-bbbb-cccc-ddddeeeeffdd\", \"title\": \"Bad book\", \"descr\": \"It is a very bad book!\"}";
mockMvc.perform(post(PRODUCTS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(delete(PRODUCTS_URI + "(" + id + ")").content("")).andExpect(status().isNoContent());
mockMvc.perform(get(PRODUCTS_URI + "(" + id + ")").content("")).andExpect(status().isNotFound());
}
@Test
public void testGetProductWhenProductIDNotPresent() throws Exception {
final String data = "{\"ID\": \"12345678-aaaa-bbbb-cccc-ddddeeeeffaa\", \"title\": \"Another book\", \"descr\": \"It is bad book!\"}";
mockMvc.perform(post(PRODUCTS_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(get(PRODUCTS_URI + "(123)").content("")).andExpect(status().isBadRequest());
}
@Test
public void testPostCategories() throws Exception {
final String data = "{\"ID\": 2, \"parent_ID\": 1, \"name\": \"Fiction\"}";
mockMvc.perform(post(CATEGORIES_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
}
@Test
public void testEditCategories() throws Exception {
final String data = "{\"ID\": 3, \"parent_ID\": 1, \"name\": \"Fiction\"}";
final String changedData = "{\"ID\": 3, \"name\": \"Science Fiction\"}";
mockMvc.perform(post(CATEGORIES_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
mockMvc.perform(put(CATEGORIES_URI + "(3)").contentType(APPLICATION_JSON_UTF8).content(changedData))
.andExpect(status().isOk());
}
@Test
public void testPostCurrencies() throws Exception {
final String data = "{\"code\": \"USD\", \"symbol\": \"$\", \"name\": \"US Dollar\", \"descr\": \"United States Dollar\"}";
mockMvc.perform(post(CURRENCIES_URI).contentType(APPLICATION_JSON_UTF8).content(data))
.andExpect(status().isCreated());
}
}

View File

@@ -0,0 +1,7 @@
---
spring:
profiles: default
datasource:
url: "jdbc:sqlite::memory:"
driver-class-name: org.sqlite.JDBC
initialization-mode: always