akka-http: How can I consume/hide an extracted value? - akka-http

I am trying to write a directive that rejects non-ajax requests. The code below doesn't work obviously:
import akka.http.scaladsl.model.HttpHeader
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.directives.BasicDirectives
import akka.http.scaladsl.server.directives.HeaderDirectives
import akka.http.scaladsl.server.directives.RouteDirectives
trait AjaxDirectives extends BasicDirectives with HeaderDirectives with RouteDirectives {
private val valid = "XMLHttpRequest"
def ajax(): Directive0 = {
headerValueByName("X-Requested-With") { header ⇒
if (header == valid) {
pass
} else {
reject
}
}
}
}
(2 problems here: pass is Directive0 & headerValueByName is Directive1, and headerValueByName is Directive1 & ajax is Directive0. So it does not compile)
My question is: can I somehow have a locally scoped extract? As in, header doesn't escape ajax.
I know I can access the request to pull the header out, without using headerValue*, so please don't answer with that.

Reading SecurityDirectives.authorizeAsync gave me the answer:
import akka.http.scaladsl.model.HttpHeader
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.directives.BasicDirectives
import akka.http.scaladsl.server.directives.HeaderDirectives
import akka.http.scaladsl.server.directives.RouteDirectives
trait AjaxDirectives extends BasicDirectives with HeaderDirectives with RouteDirectives {
private val headerName = "X-Requested-With"
private val valid = "XMLHttpRequest"
def ajax(): Directive0 = {
headerValueByName(headerName).flatMap { header ⇒
if (header == valid) {
pass
} else {
reject
}
}
}
}

Related

JsonbDeserializer's deserialize method does NOT read existing element

Currently I'm using WildFly 21.0.2 and JSON-B and JSON-P APIs. The Yasson version in WildFly modules is 1.0.5. I have the following JSON coming from REST endpoint:
{
"circuitInfoResponseList": [
{
"org.my.company.dto.FiberCircuitInfoResponse": {
"attendanceType": "BY_RADIUS",
"index": 0,
...
This is my JsonbDeserializer implementation:
public CircuitInfoResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext, Type type) {
jsonParser.next();
String className = jsonParser.getString();
jsonParser.next();
try {
return deserializationContext.deserialize(Class.forName(className).asSubclass(CircuitInfoResponse.class), jsonParser);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new JsonbException("Cannot deserialize object.");
}
//return deserializationContext.deserialize(FiberCircuitInfoResponse.class, jsonParser);
}
This method gets the SECOND entry from the json attendanceType and NOT the desired org.my.company.dto.FiberCircuitInfoResponse. BTW... when I serialize the JSON Object I can see the string org.my.company.dto.FiberCircuitInfoResponse however when it arrives and the client side it does NOT contain that string. It comes likes this:
[
{
"circuitInfoResponseList": [
{
"attendanceType": "BY_RADIUS",
"index": 0,
Without that information I cannot tell which subclass to create. I've already tried to follow this tips but without success:
https://javaee.github.io/jsonb-spec/users-guide.html
https://github.com/m0mus/JavaOne2016-JSONB-Demo/blob/4ecc22f69d57fda765631237d897b0a487f58d90/src/main/java/com/oracle/jsonb/demo/serializer/AnimalDeserializer.java
https://javaee.github.io/javaee-spec/javadocs/javax/json/bind/serializer/JsonbDeserializer.html
These are my POJO classes.
Parent class:
import lombok.*;
import lombok.experimental.SuperBuilder;
import javax.json.bind.annotation.JsonbTypeDeserializer;
#Data
#SuperBuilder
#NoArgsConstructor(access = AccessLevel.PROTECTED)
#JsonbTypeDeserializer(CircuitInfoResponseJsonbXerializer.class)
public class CircuitInfoResponse {
...
}
Child class:
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
#Data
#SuperBuilder
#EqualsAndHashCode(callSuper = false)
#NoArgsConstructor(access = AccessLevel.PROTECTED)
public class FiberCircuitInfoResponse extends CircuitInfoResponse {
...
}
Serialize code:
Type responseListType = new ArrayList<SimulationServiceResponse>() {}.getClass().getGenericSuperclass();
JsonbConfig config = new JsonbConfig()
.withSerializers(new CircuitInfoResponseJsonbXerializer());
Jsonb jsonb = JsonbBuilder.create(config);
String json = jsonb.toJson(response, responseListType);
System.out.println(json);
return Response.status(Response.Status.OK).entity(json).build();
Deserialize code:
String restJsonResponse = restResponse.readEntity(String.class);
JsonbConfig config = new JsonbConfig()
.withDeserializers(new CircuitInfoResponseJsonbXerializer());
Jsonb jsonbCustom = JsonbBuilder.create(config);
List<SimulationServiceResponse> restResponseEntity = jsonbCustom.fromJson(restJsonResponse, new ArrayList<SimulationServiceResponse>() {}.getClass().getGenericSuperclass());
This is the class that contains a list of Parent class above:
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
#Data
public class SimulationServiceResponse {
...
#Getter(AccessLevel.NONE)
#Setter(AccessLevel.NONE)
private List<CircuitInfoResponse> circuitInfoResponseList;
public List<CircuitInfoResponse> getCircuitInfoResponseList() {
if (circuitInfoResponseList == null) {
circuitInfoResponseList = new ArrayList<>();
}
return circuitInfoResponseList;
}
public void setCircuitInfoResponseList(List<CircuitInfoResponse> list) {
this.circuitInfoResponseList = list;
}
}
Do you guys have any idea of what I'm doing wrong?

Liferay 7.2 api json url 404

I have:
Eclispse
Version: 2019-06 (4.12.0)
Build id: 20190614-1200
Liferay 7.2.0 CE GA1.
I use Gradle.
I followed this tutorial:
https://www.liferaystack.com/2017/11/rest-extender-and-jax-rs-restful-web-service-in-liferay-7-dxp.html
I created a rest module.
I created two file of configuration inside the folder "src/main/resources/configuration":
com.liferay.portal.remote.cxf.common.configuration.CXFEndpointPublisherConfiguration-cxf.properties
Code:
contextPath=/my-rest-service
authVerifierProperties=auth.verifier.BasicAuthHeaderAuthVerifier.urls.includes=*
com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration-rest.properties
Code:
contextPaths=/my-rest-service
jaxRsServiceFilterStrings=(component.name=com.liferaystack.application.MyRestServiceApplication)
This is the MyRestWebserviceApplication.java:
package com.liferaystack.application;
import java.util.Collections;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Application;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
/**
* #author pinoteam
*/
#ApplicationPath("/my-rest-service")
#Component(immediate = true, service = Application.class)
public class MyRestWebserviceApplication extends Application {
public Set<Object> getSingletons() {
return Collections.<Object>singleton(this);
}
#GET
#Produces("text/plain")
public String working() {
return "It works!";
}
#GET
#Path("/morning")
#Produces("text/plain")
public String hello() {
return "Good morning!";
}
#GET
#Path("/mattina")
#Produces("text/plain")
public String helloGa() {
return "Good morning!";
}
#GET
#Path("/morning/{name}")
#Produces("text/plain")
public String morning(
#PathParam("name") String name,
#QueryParam("drink") String drink) {
String greeting = "Good Morning " + name;
if (drink != null) {
greeting += ". Would you like some " + drink + "?";
}
return greeting;
}
}
I run build and deploy.
The app is active, but nothing work.
In control panel i have not any Rest Extender Configuration and i have not api at any url.
what am i missing? any idea?
The tutorial you link is for Liferay 7.0 - the package/path names of the configuration files might have changed, and I've seen a context path being declared in bnd.bnd, rather than those property files - things might have changed since then.
An alternative starting point might be the blade sample, which is written for 7.2, and provides a Whiteboard-properties in the Java class:
#Component(
property = {
JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE + "=/users",
JaxrsWhiteboardConstants.JAX_RS_NAME + "=Users"
},
service = Application.class
)
public class UsersRestService extends Application {
#GET
#Path("/list")
#Produces("text/plain")
public String getUsers() {
...
}
...
That plugin's configuration file is rather trivial (looks optional on first glance, but don't take my word for it)

How to get the Base URL including the context for the storefront?

How to get the Base URL including the context for the storefront?
For example, for this URL
https://spartacus.c39j2-walkersde1-d4-public.model-t.cc.commerce.ondemand.com/electronics-spa/en/USD/OpenCatalogue/Cameras/Digital-Cameras/Digital-SLR/c/578
How do I get just
https://spartacus.c39j2-walkersde1-d4-public.model-t.cc.commerce.ondemand.com/electronics-spa/en/USD/
You can get separately the origin (https://s(...).com) and the context URL parameters (electronics-spa/en/USD/).
The origin
import { DOCUMENT } from '#angular/common';
// ...
constructor(#Inject(DOCUMENT) private document: any){}
//...
const origin = this.document.location.origin;
// "https://s(...).com"
Note: it won't work in Server Side Rendering! Please use Spartacus' injection token SERVER_REQUEST_ORIGIN then:
import { DOCUMENT } from '#angular/common';
import { PLATFORM_ID } from '#angular/core';
import { SERVER_REQUEST_OTIGIN } from '#spartacus/core';
// ...
constructor(
#Inject(DOCUMENT) private document: any,
#Inject(PLATFORM_ID) private platformId,
#Inject(SERVER_REQUEST_ORIGIN) private serverRequestOrigin
){}
// ...
let origin;
if (isPlatformServer(this.platformId)){
origin = this.serverRequestOrigin;
} else {
origin = this.document.location.origin;
}
The context URL parameters
import { Router } from '#angular/router';
// ...
constructor(private router: Router){}
// ...
const context = router.serializeUrl(router.createUrlTree(''));
// "electronics-spa/en/USD/"
Final result
const result = `${origin}/${context}`;
// "https://s(...).com/electronics-spa/en/USD/"

How do you use the LauncherDiscoveryRequestBuilder to execute a test method that has a parameter of type TestInfo?

I tried out all the different method selectors as seen on this page: https://junit.org/junit5/docs/current/api/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.html
For example tried to do it like so:
selectMethod("org.example.order.OrderTests#test3"),
like so:
selectMethod("org.example.order.OrderTests#test3(TestInfo)"),
or like so: selectMethod("org.example.order.OrderTests#test3(org.junit.jupiter.engine.extension.TestInfoParameterResolver$DefaultTestInfo)")
Each time, no tests are found.
When I only select the class the method resides in, it works: selectClass("org.example.order.OrderTests")
(but I'm looking to call the method explicitly)
I am assuming the behavior is the same for other parameter types that are resolved at runtime by a ParameterResolver.
Your assumption is wrong. You can select one and only one test method.
As you mentioned on this page Discovery Selectors there are a lot of examples.
DiscoverySelectors.selectMethod provide three way to select desired method(s)
public static MethodSelector selectMethod(String className, String methodName, String methodParameterTypes) {
...
}
public static MethodSelector selectMethod(String className, String methodName) {
...
}
and
public static MethodSelector selectMethod(String fullyQualifiedMethodName) throws PreconditionViolationException {
...
}
You've tried to use the last method but the fullyQualifiedMethodName was wrong a little bit. If you take a look on javadoc it will turn up.
Parameter type list must exactly match and every non-primitive types must be fully qualified as well.
In your example the package is missing. Try it like: selectMethod("org.example.order.OrderTests#test3(org.junit.jupiter.api.TestInfo)")
Here is a short test.
package io.github.zforgo.stackoverflow;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
public class ClassWithTestInfo {
#Test
void foo() {
}
#Test
void foo(TestInfo info) {
}
#RepeatedTest(3)
void foo(RepetitionInfo info) {
}
}
package io.github.zforgo.stackoverflow;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.engine.FilterResult;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
class DiscoveryTest {
#Test
#DisplayName("Should select only the desired method(s)")
void doTEst() {
Assertions.assertAll(
() -> {
var methods = discover(DiscoverySelectors.selectClass(ClassWithTestInfo.class));
Assertions.assertEquals(3, methods.size());
},
() -> {
// your way
var fqmn = "io.github.zforgo.stackoverflow.ClassWithTestInfo#foo(TestInfo)";
var methods = discover(DiscoverySelectors.selectMethod(fqmn));
Assertions.assertEquals(0, methods.size());
},
() -> {
// good way
var fqmn = "io.github.zforgo.stackoverflow.ClassWithTestInfo#foo(org.junit.jupiter.api.TestInfo)";
var methods = discover(DiscoverySelectors.selectMethod(fqmn));
Assertions.assertEquals(1, methods.size());
}
);
}
private List<Method> discover(DiscoverySelector... selectors) {
final List<Method> methodCollector = new ArrayList<>();
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectors)
.filters((PostDiscoveryFilter) object -> {
Method m = ((MethodBasedTestDescriptor) object).getTestMethod();
methodCollector.add(m);
return FilterResult.included("Matched");
})
.build();
LauncherFactory.create().discover(request);
return methodCollector;
}
}

Spring restController: how to error when unknown #RequestParam is in url

I'm using spring 4.2 to create some restfull webservices.
But we realized that when a user mistypes one of the not-mandatory #RequestParam, we do not get an error that the param he passed is unknown.
like we have #RequestParam(required=false, value="valueA") String value A and in the call he uses '?valuueA=AA' -> we want an error.
But I do not seem to find a way to do this, the value is just ignored and the user is unaware of this.
One possible solution would be to create an implementation of HandlerInterceptor which will verify that all request parameters passed to the handler method are declared in its #RequestParam annotated parameters.
However you should consider the disadvantages of such solution. There might be situations where you want to allow certain parameters to be passed in and not be declared as request params. For instance if you have request like GET /foo?page=1&offset=0 and have handler with following signature:
#RequestMapping
public List<Foo> listFoos(PagingParams page);
and PagingParams is a class containing page and offset properties, it will normally be mapped from the request parameters. Implementation of a solution you want would interfere with this Spring MVC'c functionality.
That being said, here is a sample implementation I had in mind:
public class UndeclaredParamsHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
checkParams(request, getDeclaredRequestParams(handlerMethod));
}
return true;
}
private void checkParams(HttpServletRequest request, Set<String> allowedParams) {
request.getParameterMap().entrySet().forEach(entry -> {
String param = entry.getKey();
if (!allowedParams.contains(param)) {
throw new UndeclaredRequestParamException(param, allowedParams);
}
});
}
private Set<String> getDeclaredRequestParams(HandlerMethod handlerMethod) {
Set<String> declaredRequestParams = new HashSet<>();
MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
for (MethodParameter methodParameter : methodParameters) {
if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
if (StringUtils.hasText(requestParam.value())) {
declaredRequestParams.add(requestParam.value());
} else {
methodParameter.initParameterNameDiscovery(parameterNameDiscoverer);
declaredRequestParams.add(methodParameter.getParameterName());
}
}
}
return declaredRequestParams;
}
}
Basically this will do what I described above. You can then add exception handler for the exception it throws and translate it to HTTP 400 response. I've put more of an complete sample on Github, which includes a way to selectively enable this behavior for individual handler methods via annotation.
I translated Bohuslav Burghardt's solution for Spring WebFlux applications.
I dropped the #DisallowUndeclaredRequestParams annotation class from GitHub because I didn't need it -- it just applies the filter to all HandlerMethods. But someone else could update this answer and put it back.
package com.example.springundeclaredparamerror;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
/**
* Handler interceptor used for ensuring that no request params other than those explicitly
* declared via {#link RequestParam} parameters of the handler method are passed in.
*/
// Implementation translated into WebFlux WebFilter from:
// https://github.com/bohuslav-burghardt/spring-sandbox/tree/master/handler-interceptors/src/main/java/handler_interceptors
#Component
public class DisallowUndeclaredParamsFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(DisallowUndeclaredParamsFilter.class);
#Autowired
#Qualifier("requestMappingHandlerMapping")
RequestMappingHandlerMapping mapping;
#Autowired
ObjectMapper mapper;
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
Object o = mapping.getHandler(serverWebExchange).toFuture().getNow(null);
Optional<String> undeclaredParam = Optional.empty();
if (o != null && o instanceof HandlerMethod) {
var handlerMethod = (HandlerMethod) o;
undeclaredParam = checkParams(serverWebExchange.getRequest(),
getDeclaredRequestParams(handlerMethod));
}
return undeclaredParam.map((param) -> RespondWithError(serverWebExchange, param))
.orElseGet(() -> webFilterChain.filter(serverWebExchange));
}
/** Responds to the request with an error message for the given undeclared parameter. */
private Mono<Void> RespondWithError(ServerWebExchange serverWebExchange, String undeclaredParam) {
final HttpStatus status = HttpStatus.BAD_REQUEST;
serverWebExchange.getResponse().setStatusCode(status);
serverWebExchange.getResponse().getHeaders().add(
"Content-Type", "application/json");
UndeclaredParamErrorResponse response = new UndeclaredParamErrorResponse();
response.message = "Parameter not expected: " + undeclaredParam;
response.statusCode = status.value();
String error = null;
try {
error = mapper.writeValueAsString(response);
} catch (JsonProcessingException e) {
error = "Parameter not expected; error generating JSON response";
LOGGER.warn("Error generating JSON response for undeclared argument", e);
}
return serverWebExchange.getResponse().writeAndFlushWith(
Mono.just(Mono.just(serverWebExchange.getResponse().bufferFactory().wrap(
error.getBytes(StandardCharsets.UTF_8)))));
}
/** Structure for generating error JSON. */
static class UndeclaredParamErrorResponse {
public String message;
public int statusCode;
}
/**
* Check that all of the request params of the specified request are contained within the specified set of allowed
* parameters.
*
* #param request Request whose params to check.
* #param allowedParams Set of allowed request parameters.
* #return Name of a param in the request that is not allowed, or empty if all params in the request are allowed.
*/
private Optional<String> checkParams(ServerHttpRequest request, Set<String> allowedParams) {
return request.getQueryParams().keySet().stream().filter(param ->
!allowedParams.contains(param)
).findFirst();
}
/**
* Extract all request parameters declared via {#link RequestParam} for the specified handler method.
*
* #param handlerMethod Handler method to extract declared params for.
* #return Set of declared request parameters.
*/
private Set<String> getDeclaredRequestParams(HandlerMethod handlerMethod) {
Set<String> declaredRequestParams = new HashSet<>();
MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
for (MethodParameter methodParameter : methodParameters) {
if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
if (StringUtils.hasText(requestParam.value())) {
declaredRequestParams.add(requestParam.value());
} else {
methodParameter.initParameterNameDiscovery(parameterNameDiscoverer);
declaredRequestParams.add(methodParameter.getParameterName());
}
}
}
return declaredRequestParams;
}
}
Here's the unit test I wrote for it. I recommend checking it into your codebase as well.
package com.example.springundeclaredparamerror;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
#RunWith(SpringRunner.class)
#WebFluxTest(controllers = {DisallowUndeclaredParamFilterTest.TestController.class})
public class DisallowUndeclaredParamFilterTest {
private static final String TEST_ENDPOINT = "/disallowUndeclaredParamFilterTest";
#Rule
public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
#Autowired
private WebTestClient webClient;
#Configuration
#Import({TestController.class, DisallowUndeclaredParamsFilter.class})
static class TestConfig {
}
#RestController
static class TestController {
#GetMapping(TEST_ENDPOINT)
public Mono<String> retrieveEntity(#RequestParam(name = "a", required = false) final String a) {
return Mono.just("ok");
}
}
#Test
public void testAllowsNoArgs() {
webClient.get().uri(TEST_ENDPOINT).exchange().expectBody(String.class).isEqualTo("ok");
}
#Test
public void testAllowsDeclaredArg() {
webClient.get().uri(TEST_ENDPOINT + "?a=1").exchange().expectBody(String.class).isEqualTo("ok");
}
#Test
public void testDisallowsUndeclaredArg() {
webClient.get().uri(TEST_ENDPOINT + "?b=1").exchange().expectStatus().is4xxClientError();
}
}