rest call test (untested)
This commit is contained in:
@@ -13,6 +13,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* Reads an .xlsx workbook (created with Excel or LibreOffice) from the classpath
|
* Reads an .xlsx workbook (created with Excel or LibreOffice) from the classpath
|
||||||
* and returns all cells as Strings: book > sheets > rows > cells.
|
* and returns all cells as Strings: book > sheets > rows > cells.
|
||||||
|
*
|
||||||
* Restrictions per sheet:
|
* Restrictions per sheet:
|
||||||
* - The column count is defined by the first row: it starts with the 1st cell
|
* - The column count is defined by the first row: it starts with the 1st cell
|
||||||
* and ends at the 1st empty cell
|
* and ends at the 1st empty cell
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import java.lang.annotation.*;
|
|||||||
@ExtendWith(TemplateExtension.class)
|
@ExtendWith(TemplateExtension.class)
|
||||||
@ExtendWith(TenantSetupExtension.class)
|
@ExtendWith(TenantSetupExtension.class)
|
||||||
@ExtendWith(TestMailServerExtension.class)
|
@ExtendWith(TestMailServerExtension.class)
|
||||||
|
@ExtendWith(TestRestServerExtension.class)
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@Transactional
|
@Transactional
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.flowable.audit.api.runtime.AuditInstance;
|
|||||||
import com.flowable.core.spring.security.SecurityUtils;
|
import com.flowable.core.spring.security.SecurityUtils;
|
||||||
import com.flowable.platform.service.task.CompleteFormRepresentation;
|
import com.flowable.platform.service.task.CompleteFormRepresentation;
|
||||||
import com.flowable.platform.service.task.PlatformTaskService;
|
import com.flowable.platform.service.task.PlatformTaskService;
|
||||||
|
import com.flowable.serviceregistry.api.runtime.ServiceInvocationResultResponse;
|
||||||
|
import com.flowable.serviceregistry.api.runtime.ServiceRegistryRuntimeService;
|
||||||
import com.github.wnameless.json.flattener.JsonFlattener;
|
import com.github.wnameless.json.flattener.JsonFlattener;
|
||||||
import com.github.wnameless.json.unflattener.JsonUnflattener;
|
import com.github.wnameless.json.unflattener.JsonUnflattener;
|
||||||
import jakarta.mail.Address;
|
import jakarta.mail.Address;
|
||||||
@@ -35,6 +37,8 @@ import org.junit.jupiter.params.provider.Arguments;
|
|||||||
import org.junit.platform.commons.util.StringUtils;
|
import org.junit.platform.commons.util.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
@@ -59,6 +63,9 @@ public class FlowableModelTestUtils {
|
|||||||
protected final FlowableExcelMapper flowableExcelMapper;
|
protected final FlowableExcelMapper flowableExcelMapper;
|
||||||
protected static final Logger logger = LoggerFactory.getLogger(FlowableModelTestUtils.class);
|
protected static final Logger logger = LoggerFactory.getLogger(FlowableModelTestUtils.class);
|
||||||
protected final TestMailServer testMailServer;
|
protected final TestMailServer testMailServer;
|
||||||
|
protected final TestRestServer testRestServer;
|
||||||
|
protected final TestRestClient testRestClient;
|
||||||
|
protected final ServiceRegistryRuntimeService serviceRegistryRuntimeService;
|
||||||
protected final AuditService auditService;
|
protected final AuditService auditService;
|
||||||
protected final ObjectMapper objectMapper = new ObjectMapper();
|
protected final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@@ -79,6 +86,9 @@ public class FlowableModelTestUtils {
|
|||||||
ManagementService managementService,
|
ManagementService managementService,
|
||||||
FlowableExcelMapper flowableExcelMapper,
|
FlowableExcelMapper flowableExcelMapper,
|
||||||
TestMailServer testMailServer,
|
TestMailServer testMailServer,
|
||||||
|
TestRestServer testRestServer,
|
||||||
|
TestRestClient testRestClient,
|
||||||
|
ServiceRegistryRuntimeService serviceRegistryRuntimeService,
|
||||||
AuditService auditService) {
|
AuditService auditService) {
|
||||||
this.processEngine = processEngine;
|
this.processEngine = processEngine;
|
||||||
this.cmmnEngine = cmmnEngine;
|
this.cmmnEngine = cmmnEngine;
|
||||||
@@ -87,6 +97,9 @@ public class FlowableModelTestUtils {
|
|||||||
this.managementService = managementService;
|
this.managementService = managementService;
|
||||||
this.flowableExcelMapper = flowableExcelMapper;
|
this.flowableExcelMapper = flowableExcelMapper;
|
||||||
this.testMailServer = testMailServer;
|
this.testMailServer = testMailServer;
|
||||||
|
this.testRestServer = testRestServer;
|
||||||
|
this.testRestClient = testRestClient;
|
||||||
|
this.serviceRegistryRuntimeService = serviceRegistryRuntimeService;
|
||||||
this.auditService = auditService;
|
this.auditService = auditService;
|
||||||
// Enable ObjectMapper for handling Instant as string
|
// Enable ObjectMapper for handling Instant as string
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
@@ -157,6 +170,25 @@ public class FlowableModelTestUtils {
|
|||||||
.containsExactlyInAnyOrder(receivers.split("[,\\s]+"));
|
.containsExactlyInAnyOrder(receivers.split("[,\\s]+"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkAndAssertRestRequest(Map<String, Object> map, int restNumber, String hint, String method, String path) {
|
||||||
|
if (map != null) {
|
||||||
|
Object check = map.get("rest");
|
||||||
|
if (check instanceof Integer && (Integer) check > 0) {
|
||||||
|
assertRestRequest(restNumber, hint, method, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertRestRequest(int restNumber, String hint, String method, String path) {
|
||||||
|
if (restNumber <= 0) return;
|
||||||
|
List<RestRequestDto> restRequests = getRestRequestList();
|
||||||
|
Assertions.assertThat(restRequests.size()).as("%s: invalid rest request number %s", hint, restNumber).isGreaterThanOrEqualTo(restNumber);
|
||||||
|
|
||||||
|
RestRequestDto restRequest = restRequests.get(restNumber - 1);
|
||||||
|
Assertions.assertThat(restRequest.method()).as("%s: invalid method %s", hint, restRequest.method()).isEqualTo(method);
|
||||||
|
Assertions.assertThat(restRequest.path()).as("%s: invalid path %s", hint, restRequest.path()).isEqualTo(path);
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> getHistoryCasePayload(String caseInstanceId) {
|
public Map<String, Object> getHistoryCasePayload(String caseInstanceId) {
|
||||||
List<HistoricVariableInstance> historicVariableInstanceList = cmmnEngine.getCmmnHistoryService()
|
List<HistoricVariableInstance> historicVariableInstanceList = cmmnEngine.getCmmnHistoryService()
|
||||||
.createHistoricVariableInstanceQuery()
|
.createHistoricVariableInstanceQuery()
|
||||||
@@ -187,6 +219,15 @@ public class FlowableModelTestUtils {
|
|||||||
.start();
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CaseInstance getCaseInstance(String caseDefinitionKey) {
|
||||||
|
CaseInstance caseInstance = cmmnEngine.getCmmnRuntimeService()
|
||||||
|
.createCaseInstanceQuery()
|
||||||
|
.caseDefinitionKey(caseDefinitionKey)
|
||||||
|
.singleResult();
|
||||||
|
Assertions.assertThat(caseInstance).as("No case instance with definition key %s found", caseDefinitionKey).isNotNull();
|
||||||
|
return caseInstance;
|
||||||
|
}
|
||||||
|
|
||||||
public Task getOpenTask(String taskKey) {
|
public Task getOpenTask(String taskKey) {
|
||||||
Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult();
|
Task task = taskService.createTaskQuery().taskDefinitionKey(taskKey).singleResult();
|
||||||
Assertions.assertThat(task).as("No open task with key %s found", taskKey).isNotNull();
|
Assertions.assertThat(task).as("No open task with key %s found", taskKey).isNotNull();
|
||||||
@@ -221,6 +262,31 @@ public class FlowableModelTestUtils {
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<RestRequestDto> getRestRequestList() {
|
||||||
|
return testRestServer.getRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stubRestResponse(String method, String path, int status, String body) {
|
||||||
|
testRestServer.stubResponse(method, path, status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> sendRestEvent(String channelKey, ObjectNode payload) {
|
||||||
|
return testRestClient.sendChannelEvent(channelKey, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> sendRestCall(HttpMethod method, String path, Object body) {
|
||||||
|
return testRestClient.exchange(method, path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceInvocationResultResponse invokeRestService(String serviceKey, String operationKey, Map<String, Object> serviceData) {
|
||||||
|
return serviceRegistryRuntimeService.createServiceInvocationBuilder()
|
||||||
|
.serviceKey(serviceKey)
|
||||||
|
.operationKey(operationKey)
|
||||||
|
.serviceData(serviceData)
|
||||||
|
.tenantId(TENANT_ID) // Must not be "default"
|
||||||
|
.invoke();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSubset(JsonNode root, JsonNode test) {
|
public boolean isSubset(JsonNode root, JsonNode test) {
|
||||||
// If test is null, it is always a subset of root
|
// If test is null, it is always a subset of root
|
||||||
if (test == null || test.isNull()) {
|
if (test == null || test.isNull()) {
|
||||||
@@ -326,9 +392,12 @@ public class FlowableModelTestUtils {
|
|||||||
Integer emailCount = null;
|
Integer emailCount = null;
|
||||||
JsonNode emailNode = row.get("email");
|
JsonNode emailNode = row.get("email");
|
||||||
if (emailNode != null) emailCount = emailNode.asInt();
|
if (emailNode != null) emailCount = emailNode.asInt();
|
||||||
|
Integer restCount = null;
|
||||||
|
JsonNode restNode = row.get("rest");
|
||||||
|
if (restNode != null) restCount = restNode.asInt();
|
||||||
|
|
||||||
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
|
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} rest={} ",
|
||||||
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
|
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount, restCount);
|
||||||
Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull();
|
Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull();
|
||||||
ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), convertJsonNodeToMap(vars));
|
ObjectNode processIds = createRootTestProcessInstance(idParam.asText(), convertJsonNodeToMap(vars));
|
||||||
for (int i = 0; i < timerCount; i++) {
|
for (int i = 0; i < timerCount; i++) {
|
||||||
@@ -353,6 +422,11 @@ public class FlowableModelTestUtils {
|
|||||||
Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount);
|
Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount);
|
||||||
result.set("emails", objectMapper.valueToTree(Collections.singletonList(emailList)));
|
result.set("emails", objectMapper.valueToTree(Collections.singletonList(emailList)));
|
||||||
}
|
}
|
||||||
|
if (restCount != null) {
|
||||||
|
List<RestRequestDto> restRequests = getRestRequestList();
|
||||||
|
Assertions.assertThat(restRequests.size()).as("Invalid number of rest requests: %s", restRequests.size()).isEqualTo(restCount);
|
||||||
|
result.set("restRequests", objectMapper.valueToTree(Collections.singletonList(restRequests)));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,9 +455,12 @@ public class FlowableModelTestUtils {
|
|||||||
Integer emailCount = null;
|
Integer emailCount = null;
|
||||||
Object emailNode = row.get("email");
|
Object emailNode = row.get("email");
|
||||||
if (emailNode != null) emailCount = (Integer) emailNode;
|
if (emailNode != null) emailCount = (Integer) emailNode;
|
||||||
|
Integer restCount = null;
|
||||||
|
Object restNode = row.get("rest");
|
||||||
|
if (restNode != null) restCount = (Integer) restNode;
|
||||||
|
|
||||||
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} ",
|
logger.info("TestExcelRow file={} id={} rootParam={} inParam={} outParam={} timer={} test={} audit={} email={} rest={} ",
|
||||||
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount);
|
path, idParam, rootParam, inParam, outParam, timerCount, test, auditRecordCount, emailCount, restCount);
|
||||||
Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull();
|
Assertions.assertThat(idParam).as("Column 'id' missing in row of %s", path).isNotNull();
|
||||||
ObjectNode processIds = createRootTestProcessInstance(idParam, vars);
|
ObjectNode processIds = createRootTestProcessInstance(idParam, vars);
|
||||||
for (int i = 0; i < timerCount; i++) {
|
for (int i = 0; i < timerCount; i++) {
|
||||||
@@ -408,6 +485,11 @@ public class FlowableModelTestUtils {
|
|||||||
Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount);
|
Assertions.assertThat(emailList.size()).as("Invalid number of emails: %s", emailList.size()).isEqualTo(emailCount);
|
||||||
result.put("email", emailCount);
|
result.put("email", emailCount);
|
||||||
}
|
}
|
||||||
|
if (restCount != null) {
|
||||||
|
List<RestRequestDto> restRequests = getRestRequestList();
|
||||||
|
Assertions.assertThat(restRequests.size()).as("Invalid number of rest requests: %s", restRequests.size()).isEqualTo(restCount);
|
||||||
|
result.put("rest", restCount);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.customer.work.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record RestRequestDto(String method, String path, String query, Map<String, List<String>> headers, String body) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.customer.work.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import org.springframework.web.servlet.DispatcherServlet;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test client simulating incoming REST calls which are processed in Flowable models. The
|
||||||
|
* requests are executed against the real Flowable REST API (including the security filter
|
||||||
|
* chain) but in-process via MockMvc, so they participate in the test transaction. Base URL
|
||||||
|
* and basic auth are taken from the application configuration (see 'test.rest.in.*').
|
||||||
|
*
|
||||||
|
* The Flowable REST APIs are served by their own dispatcher servlets (/platform-api/*,
|
||||||
|
* /process-api/*, ...) which are never initialized in the mock web environment. Requests
|
||||||
|
* are therefore routed by path prefix to the matching servlet registration, whose child
|
||||||
|
* application context is refreshed on first use.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TestRestClient {
|
||||||
|
private final WebApplicationContext webApplicationContext;
|
||||||
|
private final Filter springSecurityFilterChain;
|
||||||
|
private final String baseUrl;
|
||||||
|
private final String basicAuthHeader;
|
||||||
|
private final Map<String, MockMvc> mockMvcByServletPath = new ConcurrentHashMap<>();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public TestRestClient(WebApplicationContext webApplicationContext,
|
||||||
|
@Qualifier("springSecurityFilterChain") Filter springSecurityFilterChain,
|
||||||
|
@Value("${test.rest.in.base-url}") String baseUrl,
|
||||||
|
@Value("${test.rest.in.username}") String username,
|
||||||
|
@Value("${test.rest.in.password}") String password) {
|
||||||
|
this.webApplicationContext = webApplicationContext;
|
||||||
|
this.springSecurityFilterChain = springSecurityFilterChain;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.basicAuthHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> get(String path) {
|
||||||
|
return exchange(HttpMethod.GET, path, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> post(String path, Object body) {
|
||||||
|
return exchange(HttpMethod.POST, path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> exchange(HttpMethod method, String path, Object body) {
|
||||||
|
try {
|
||||||
|
String servletPath = findServletPath(path);
|
||||||
|
MockMvc mockMvc = mockMvcByServletPath.computeIfAbsent(servletPath, this::createMockMvc);
|
||||||
|
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(method, URI.create(baseUrl + path))
|
||||||
|
.servletPath(servletPath)
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, basicAuthHeader);
|
||||||
|
if (body != null) {
|
||||||
|
builder.contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsBytes(body));
|
||||||
|
}
|
||||||
|
MvcResult result = mockMvc.perform(builder).andReturn();
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
for (String name : result.getResponse().getHeaderNames()) {
|
||||||
|
headers.addAll(name, result.getResponse().getHeaders(name));
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(result.getResponse().getStatus())
|
||||||
|
.headers(headers)
|
||||||
|
.body(new String(result.getResponse().getContentAsByteArray(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Cannot execute " + method + " " + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseEntity<String> sendChannelEvent(String channelKey, Object payload) {
|
||||||
|
return post("/platform-api/channel-definitions/key/" + channelKey + "/events", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String findServletPath(String path) {
|
||||||
|
for (ServletRegistrationBean<?> registration : webApplicationContext.getBeansOfType(ServletRegistrationBean.class).values()) {
|
||||||
|
for (String urlMapping : registration.getUrlMappings()) {
|
||||||
|
if (urlMapping.endsWith("/*") && path.startsWith(urlMapping.substring(0, urlMapping.length() - 1))) {
|
||||||
|
return urlMapping.substring(0, urlMapping.length() - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MockMvc createMockMvc(String servletPath) {
|
||||||
|
WebApplicationContext context = webApplicationContext;
|
||||||
|
if (!servletPath.isEmpty()) {
|
||||||
|
context = findServletContext(servletPath);
|
||||||
|
}
|
||||||
|
return MockMvcBuilders.webAppContextSetup(context)
|
||||||
|
.addFilters(springSecurityFilterChain)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebApplicationContext findServletContext(String servletPath) {
|
||||||
|
for (ServletRegistrationBean<?> registration : webApplicationContext.getBeansOfType(ServletRegistrationBean.class).values()) {
|
||||||
|
if (registration.getUrlMappings().contains(servletPath + "/*")
|
||||||
|
&& registration.getServlet() instanceof DispatcherServlet dispatcherServlet) {
|
||||||
|
WebApplicationContext servletContext = dispatcherServlet.getWebApplicationContext();
|
||||||
|
if (servletContext instanceof ConfigurableWebApplicationContext configurableContext && !configurableContext.isActive()) {
|
||||||
|
configurableContext.setServletContext(webApplicationContext.getServletContext());
|
||||||
|
configurableContext.refresh();
|
||||||
|
}
|
||||||
|
return servletContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No dispatcher servlet registered for " + servletPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package com.customer.work.model;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test server receiving outgoing REST calls sent from Flowable models. The models resolve
|
||||||
|
* their base URL from the application configuration (see 'test.rest.out.base-url'), so all
|
||||||
|
* outgoing calls end up here, get recorded and are answered with stubbed responses.
|
||||||
|
* Requests must carry basic auth matching the configured credentials, otherwise 401 is returned.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TestRestServer {
|
||||||
|
private HttpServer server;
|
||||||
|
private final List<RestRequestDto> requests = new CopyOnWriteArrayList<>();
|
||||||
|
private final List<StubbedResponse> stubs = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private final String baseUrl;
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
public TestRestServer(@Value("${test.rest.out.base-url}") String baseUrl,
|
||||||
|
@Value("${test.rest.out.username}") String username,
|
||||||
|
@Value("${test.rest.out.password}") String password) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setup() {
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(baseUrl);
|
||||||
|
server = HttpServer.create(new InetSocketAddress(uri.getHost(), uri.getPort()), 0);
|
||||||
|
server.createContext("/", this::handle);
|
||||||
|
server.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Cannot start TestRestServer on " + baseUrl, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop(0);
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
requests.clear();
|
||||||
|
stubs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stubResponse(String method, String path, int status, String body) {
|
||||||
|
stubs.add(new StubbedResponse(method, path, status, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RestRequestDto> getRequests() {
|
||||||
|
if (server == null) {
|
||||||
|
throw new IllegalStateException("TestRestServer is not started, is the TestRestServerExtension registered?");
|
||||||
|
}
|
||||||
|
return new ArrayList<>(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handle(HttpExchange exchange) throws IOException {
|
||||||
|
try {
|
||||||
|
String method = exchange.getRequestMethod();
|
||||||
|
URI uri = exchange.getRequestURI();
|
||||||
|
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
requests.add(new RestRequestDto(method, uri.getPath(), uri.getQuery(), new LinkedHashMap<>(exchange.getRequestHeaders()), body));
|
||||||
|
|
||||||
|
if (!isAuthorized(exchange)) {
|
||||||
|
send(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StubbedResponse stub = findStub(method, uri.getPath());
|
||||||
|
if (stub != null) {
|
||||||
|
send(exchange, stub.status(), stub.body());
|
||||||
|
} else {
|
||||||
|
send(exchange, 200, "{}");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
exchange.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isAuthorized(HttpExchange exchange) {
|
||||||
|
if (username == null || username.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String expected = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||||
|
return expected.equals(exchange.getRequestHeaders().getFirst("Authorization"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected StubbedResponse findStub(String method, String path) {
|
||||||
|
return stubs.stream()
|
||||||
|
.filter(stub -> stub.method().equalsIgnoreCase(method) && path.startsWith(stub.path()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(HttpExchange exchange, int status, String body) throws IOException {
|
||||||
|
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||||
|
exchange.sendResponseHeaders(status, bytes.length);
|
||||||
|
exchange.getResponseBody().write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StubbedResponse(String method, String path, int status, String body) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.customer.work.model;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
|
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||||
|
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
|
public class TestRestServerExtension implements BeforeEachCallback, AfterEachCallback {
|
||||||
|
@Override
|
||||||
|
public void beforeEach(@NonNull ExtensionContext context) {
|
||||||
|
getTestRestServer(context).setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterEach(@NonNull ExtensionContext context) {
|
||||||
|
getTestRestServer(context).stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TestRestServer getTestRestServer(ExtensionContext context) {
|
||||||
|
return SpringExtension.getApplicationContext(context).getBean(TestRestServer.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ package com.customer.work.model.test;
|
|||||||
import com.customer.work.model.EmailDto;
|
import com.customer.work.model.EmailDto;
|
||||||
import com.customer.work.model.FlowableModelTest;
|
import com.customer.work.model.FlowableModelTest;
|
||||||
import com.customer.work.model.FlowableModelTestUtils;
|
import com.customer.work.model.FlowableModelTestUtils;
|
||||||
|
import com.customer.work.model.RestRequestDto;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.flowable.serviceregistry.api.runtime.ServiceInvocationResultResponse;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
import org.flowable.cmmn.api.runtime.CaseInstance;
|
import org.flowable.cmmn.api.runtime.CaseInstance;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
@@ -13,6 +15,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -76,6 +79,35 @@ public class ModelTest {
|
|||||||
.hasMessageContaining("Implicit parameter 'param'");
|
.hasMessageContaining("Implicit parameter 'param'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void c002RestInboundTest() {
|
||||||
|
// Simulate an incoming REST call to the inbound channel TST_CH001, processed by case TST_C002
|
||||||
|
ObjectNode payload = flowableModelTest.emptyNode();
|
||||||
|
payload.put("param", "my event payload");
|
||||||
|
ResponseEntity<String> response = flowableModelTest.sendRestEvent("TST_CH001", payload);
|
||||||
|
Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).as("response: %s", response.getBody()).isTrue();
|
||||||
|
|
||||||
|
CaseInstance caseInstance = flowableModelTest.getCaseInstance("TST_C002");
|
||||||
|
Map<String, Object> casePayload = flowableModelTest.getRuntimeCasePayload(caseInstance.getId());
|
||||||
|
// param is mapped as full payload, so it contains the complete event body
|
||||||
|
Assertions.assertThat(String.valueOf(casePayload.get("param"))).contains("my event payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sv001RestOutgoingTest() {
|
||||||
|
// The outgoing REST call from service model TST_SV001 is answered by the test REST server
|
||||||
|
flowableModelTest.stubRestResponse("GET", "/idm-api/current-user", 200,
|
||||||
|
"{\"id\":\"admin\",\"firstName\":\"Test\",\"lastName\":\"Administrator\"}");
|
||||||
|
ServiceInvocationResultResponse result = flowableModelTest.invokeRestService("TST_SV001", "getCurrentUser", Map.of());
|
||||||
|
Assertions.assertThat(result.getValue("id")).isEqualTo("admin");
|
||||||
|
Assertions.assertThat(result.getValue("lastName")).isEqualTo("Administrator");
|
||||||
|
|
||||||
|
List<RestRequestDto> restRequests = flowableModelTest.getRestRequestList();
|
||||||
|
Assertions.assertThat(restRequests).hasSize(1);
|
||||||
|
Assertions.assertThat(restRequests.get(0).method()).isEqualTo("GET");
|
||||||
|
Assertions.assertThat(restRequests.get(0).path()).isEqualTo("/idm-api/current-user");
|
||||||
|
}
|
||||||
|
|
||||||
private Stream<Arguments> p002TestData() {
|
private Stream<Arguments> p002TestData() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of("model/test/P002/boolean1.json", true, false),
|
Arguments.of("model/test/P002/boolean1.json", true, false),
|
||||||
|
|||||||
Reference in New Issue
Block a user