I have simple RestController in my application, with post mapping
#PostMapping("/bank/api/balance")
fun changeBalance(#RequestParam amount: String, #RequestParam action: String)
And I have test function for this controller.
private fun postBalanceAddRequest(amount: String): ResultActions {
return mockMvc.perform(MockMvcRequestBuilders.post(baseUrl).apply {
param("action", "add")
param("amount", amount)
})
}
Which I am using in this test
#Test
fun `add post request must return ok if request contains correct data`() {
Mockito.`when`(authenticationService.getCurrentUserUsername()).thenReturn("user")
Mockito.`when`(userService.userExist("user")).thenReturn(true)
postBalanceAddRequest(amount = "10")
.andExpect(MockMvcResultMatchers.status().isOk)
}
When I start this test I got 404 code, but it seems to me that there cannot be such a return code here.
when using MockMvc you don't need to provide the full url (including domain and port), the mockMvc will take care of it.
instead, you need just the uri. set your baseUrl to /bank/api/balance
Thanks for help, I just forgot to add a necessary controller. As for mockMvc, you can use short url, without domain and port
(I think it is the best way), but if you want you can use full url.
Related
I got this piece of code, I am learning from tutorial. I want to return an element by url which looks like clients/1 instead of clients?id=1. How can I achieve this? Also, can the code below be made easier way?
#GetMapping
public Client getClient(#RequestParam int id) {
Optional<Client> first = clientList.stream().filter(element -> element.getId() == id).findFirst();
return first.get();
}
You may want to use #PathVariable as follows:
#Controller
#RequestMapping("/clients")
public class MyController {
#GetMapping("/{id}")
public Client getClient(#PathVariable int id) {
return clientList.stream().filter(element -> element.getId() == id).findFirst().orElseThrow();
}
Please note, the Optional can be unpacked with orElseThrow method. This will throw a NoSuchElementException in case there is no element found for the id.
Other solution would be to use orElse(new Client(...)) to return a default value if nothing is found.
get() is not really recommended to be used. From the JavaDoc of the get() method:
API Note:
The preferred alternative to this method is orElseThrow().
Even though get() may also throw a NoSuchElementException, similar to orElseThrow, usually the consensus is that get should not be used without isPresent, or should not be used at all. There several other methods to unpack the Optional without forcing you write an if.
The whole idea of the Optional is to overcome this by forcing you to think about the case when there is no value inside.
Problem
We're developing a Spring Boot service to upload data to different back end databases. The idea is that, in one multipart/form-data request a user will send a "model" (basically a file) and "modelMetadata" (which is JSON that defines an object of the same name in our code).
We got the below to work in the WebFlux annotated controller syntax, when the user sends the "modelMetadata" in the multipart form with the content-type of "application/json":
#PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun saveModel(#RequestPart("modelMetadata") monoModelMetadata: Mono<ModelMetadata>,
#RequestPart("model") monoModel: Mono<FilePart>,
#RequestHeader headers: HttpHeaders) : Mono<ResponseEntity<ModelMetadata>> {
return modelService.saveModel(monoModelMetadata, monoModel, headers)
}
But we can't seem to figure out how to do the same thing in Webflux's functional router definition. Below are the relevant code snippets we have:
#Bean
fun modelRouter() = router {
accept(MediaType.MULTIPART_FORM_DATA).nest {
POST(ROOT, handler::saveModel)
}
}
fun saveModel(r: ServerRequest): Mono<ServerResponse> {
val headers = r.headers().asHttpHeaders()
val monoModelPart = r.multipartData().map { multiValueMap ->
it["model"] // What do we do with this List<Part!> to get a Mono<FilePart>
it["modelMetadata"] // What do we do with this List<Part!> to get a Mono<ModelMetadata>
}
From everything we've read, we should be able to replicate the same functionality found in the annotation controller syntax with the router functional syntax, but this particular aspect doesn't seem to be well documented. Our goal was to move over to use the new functional router syntax since this is a new application we're developing and there are some nice forward thinking features/benefits as described here.
What we've tried
Googling to the ends of the Earth for a relevant example
this is a similar question, but hasn't gained any traction and doesn't relate to our need to create an object from one piece of the multipart request data
this may be close to what we need for uploading the file component of our multipart request data, but doesn't handle the object creation from JSON
Tried looking at the #RequestPart annotation code to see how things are done on that side, there's a nice comment that seems to hint at how they are converting the parts to objects, but we weren't able to figure out where that code lives or any relevant example of how to use an HttpMessageConverter on the ``
the content of the part is passed through an {#link HttpMessageConverter} taking into consideration the 'Content-Type' header of the request part.
Any and all help would be appreciated! Even just some links for us to better understand Part/FilePart types and there role in multipart requests would be helpful!
I was able to come up with a solution to this issue using an autowired ObjectMapper. From the below solution I could turn the modelMetadata and modelPart into Monos to mirror the #RequestPart return types, but that seems ridiculous.
I was also able to solve this by creating a MappingJackson2HttpMessageConverter and turning the metadataDataBuffer into a MappingJacksonInputMessage, but this solution seemed better for our needs.
fun saveModel(r: ServerRequest): Mono<ServerResponse> {
val headers = r.headers().asHttpHeaders()
return r.multipartData().flatMap {
// We're only expecting one Part of each to come through...assuming we understand what these Parts are
if (it.getOrDefault("modelMetadata", listOf()).size == 1 && it.getOrDefault("model", listOf()).size == 1) {
val modelMetadataPart = it["modelMetadata"]!![0]
val modelPart = it["model"]!![0] as FilePart
modelMetadataPart
.content()
.map { metadataDataBuffer ->
// TODO: Only do this if the content is JSON?
objectMapper.readValue(metadataDataBuffer.asInputStream(), ModelMetadata::class.java)
}
.next() // We're only expecting one object to be serialized from the buffer
.flatMap { modelMetadata ->
// Function was updated to work without needing the Mono's of each type
// since we're mapping here
modelService.saveModel(modelMetadata, modelPart, headers)
}
}
else {
// Send bad request response message
}
}
Although this solution works, I feel like it's not as elegant as the one alluded to in the #RequestPart annotation comments. Thus I will accept this as the solution for now, but if someone has a better solution please let us know and I will accept it!
I have the following piece of code in Kotlin (using WebFlux), which I wanna test:
fun checkUser(user: People.User?): Mono<Unit> =
if (user==null) {
Mono.empty()
} else {
webClient.get().uri {
uriBuilder -> uriBuilder
//... building a URI
}.retrieve().bodyToMono(UserValidationResponse::class.java)
.doOnError {
//log something
}.map {
if (!item.isUserValid()) {
throw InvalidUserException()
}
}
}
My unit test so far looks like this:
#Test
fun `Returns error when user is invalid`() {
val user = People.User("name", "lastname", "street", "zip code")
//when
StepVerifier.create(checkUser(user))
//then
.expectError(InvalidUserException::class.java)
.verify()
}
However when I run it, it throw the following error:
io.mockk.MockKException: no answer found for: WebClient(#1).get()
at io.mockk.impl.stub.MockKStub.defaultAnswer(MockKStub.kt:90)
at io.mockk.impl.stub.MockKStub.answer(MockKStub.kt:42)
at io.mockk.impl.recording.states.AnsweringState.call(AnsweringState.kt:16)
at io.mockk.impl.recording.CommonCallRecorder.call(CommonCallRecorder.kt:53)
at io.mockk.impl.stub.MockKStub.handleInvocation(MockKStub.kt:263)
at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:25)
at io.mockk.proxy.jvm.advice.Interceptor.call(Interceptor.kt:20)
I guess the error occurs because I havent mocked WebClient(#1).get() but I am not sure how to mock it. So far I have tried:
every { webClient.get() } returns WebClient.RequestHeadersUriSpec
but it doesnt compile. The error says:
Classifier 'RequestHeadersUriSpec' does not have a companion object, and thus must be initialized here
Someone knows how I can mock WebClient(#1).get()? Thanks in advance
Basically you need something like this:
mock ResponseSpec - mock the body or error in whichever way you need for the respective test case
mock RequestHeadersUriSpec - let the retrieve() method return the ResponseSpec mock
mock WebClient - let the get() method return the RequestHeadersUriSpec mock
Here is a full example:
val response = mockk<WebClient.ResponseSpec>()
val spec = mockk<WebClient.RequestHeadersUriSpec<*>>()
val client = mockk<WebClient>()
every { response.bodyToMono(String::class.java) } returns Mono.just("Hello StackOverflow")
every { spec.retrieve() } returns response
every { client.get() } returns spec
println(client.get().retrieve().bodyToMono(String::class.java).block())
This will correctly print the Hello StackOverflow string.
Though it may be a "historical" question, I actually also had this problem recently.
Just as what Krause mentioned, the full call path of WebClient should be mocked. This means the method stream in every{} block should as the same as WebClient call. In your case, it may be something like
every{webClient.get().uri {???}.retrieve().bodyToMono(???)} returns Mono.just(...)
The next question is something about the error message io.mockk.MockKException: no answer found for: RequestBodyUriSpec(#3).uri(......). The key to the question is methods with parameters and without parameters are totally different things.
Thus, for target method, a uri(Function<UriBuilder, URI> uriFunction) is called(a lambda expression is used here to instead of Function interface). However, for mock method, a uri() method without any parameter is called. This is why the error message said , "no answer found for ...". Therefore, in order to match the mocked method, the code should be:
every{webClient.get().uri(any<java.util.function.Function<UriBuilder, URI>>()).retrieve().bodyToMono(???)} returns Mono.just(...)
Or, the any() method can be changed to the real URI which should be as the same as the target method.
Similarly, bodyToMono() should also be mocked with the correct parameter, which may be bodyToMono(any<ParameterizedTypeReference<*>>()).
Finally, the mock code may look like:
every{client.get()
.uri(any<java.util.function.Function<UriBuilder, URI>>())
.retrieve().bodyToMono(any<ParameterizedTypeReference<*>>())}
return Mono.just(...)
I am trying to call my controller's delete method:
Spring:
#RestController
#RequestMapping("/api")
#CrossOrigin
public class Controller
{
#DeleteMapping("thing/item/{name}")
public void deleteThing(#PathVariable String name)
{
System.out.println("CALL"); // Never called!!!
}
}
Angular 7:
deleteTemplate(name: string) {
const url = `host/api/thing/item/${name}`;
return this.http.delete(url);
}
I've found something about including options:
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
deleteTemplate(name: string) {
const url = `${host}/api/thing/item/${name}`; // host is not relevant, same path with GET works.
return this.http.delete(url, this.httpOptions);
}
But I don't think that it's even needed since everything I am sending is in link itself (no json).
Every time its:
WARN 15280 --- [io-8443-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]
How to fix this?
DELETE is sent with 405 error and preceeding OPTIONS (with 200 code). It reaches server and does this error.
EDIT
Above is a sample of code that is simplified and still doesn't work. I understand your comments, but host can be anything and doesn't matter here. Its localhost now and as mentioned - it works in sense of reaching endpoint.
To check if I am wrong I did this:
#Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
this.requestMappingHandlerMapping.getHandlerMethods()
.forEach((e, k) -> System.out.println(e + " OF " + k));
After startup, it printed everything I expected, including:
{GET /api/thing/item/{name}} OF public myPackage.Thing myPackage.Controller.getThing(java.lang.String)
{DELETE /api/thing/item/{name}} OF public void myPackage.Controller.deleteThing(java.lang.String)
Any ideas?
Goddamn. Turns out problem was in CSRF, mainly in hidden Angular documentation and misleading Spring filter logs.
It took me about 10h to make it work, but only in production (where Angular is on same host/port with Spring). I will try to make it work in dev, but that requires basically rewriting whole module supplied by Angular that is bound to its defaults.
Now as to the problem:
Everything starts with Spring Security when we want to use CSRF (and we do). Apparently CsrfFilter literally explodes if it can't match CSRF tokens it expects and falls back to /error. This all happens without ANY specific log message, but simple Request method 'DELETE' not supported, which from my question we know IT IS present.
This is not only for DELETE, but all action requests (non-GET/HEAD).
This all points to "How to setup CSRF with Angular?". Well, here we go:
https://angular.io/guide/http#security-xsrf-protection
And it WORKS by DEFAULT. Based on Cookie-Header mechanism for CSRF.
But wait, there's more!
Spring needs to bo configured for Cookie-Header mechanism. Happily we got:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().csrfTokenRepository(this.csrfRepo());
}
private CookieCsrfTokenRepository csrfRepo()
{
CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setCookieHttpOnly(false);
return repo;
}
Which is built-in in spring and actually uses same cookie/header names as ones used by Angular. But what's up with repo.setCookieHttpOnly(false);? Well... another poor documented thingy. You just NEED to set it to false, otherwise Angular's side will not work.
Again - this only works for production builds, because Angular doesn't support external links. More at: Angular 6 does not add X-XSRF-TOKEN header to http request
So yeah... to make it work in dev localhost with 2 servers I'll need 1st to recreate https://angular.io/api/common/http/HttpClientXsrfModule mechanism to also intercept non-default requests.
EDIT
Based on JB Nizet comment I cooked up setup that works for both dev and production. Indeed proxy is awesome. With CSRF and SSL enabled (in my case), you also need to enable SSL on Angular CLI, otherwise Angular will not be able to use CSRF of SSL-enabled proxy backend and thus not work.
"serve": {
"options": {
"proxyConfig": "proxy.conf.json",
"sslKey": "localhost.key",
"sslCert": "localhost.crt",
"ssl": true
}
}
Note that Cert and Key will be auto-generated if not found :)
For Spring backend you don't need same cert. I use separate JKS there.
Cheers!
Add a slash / at the beginning of your path.
#DeleteMapping("/thing/item/{name}")
I have two postmapping, one is, e.g. /api/register, another is /api/authenticate, after /api/register given username and password, I want to forward the request to /api/authenticate, make it unnecessary for users input username and password again, my /api/register method like:
#PostMapping("/api/register")
public String register(#RequestBody xxxx) {
...
return "forward:/api/authenticate";
}
and /api/authenticate is like:
#PostMapping("/api/authenticate")
public ResponseEntity<JWTToken> authorize(#RequestBody LoginVM loginVM) {
....
}
but it does not work, I emulate with postman and get a string like: "Expected 'a' instead of 'o'", don't know why?
Forward / redirect only works with endpoints which support Get requests so PostMapping("/api/authenticate") would not work.
The best approach you have is to call authorize method directly from your code. Something like -
#PostMapping("/api/register")
public ResponseEntity<JWTToken> register(#RequestBody xxxx) {
...
return authorize(...);
}