Spring Boot Test with Mockito : #Validated annotation is being ignored during unit tests - spring-boot

I'm using Spring Boot 2.1.1, JUnit 5, Mockito 2.23.4.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
Here's my controller :
#RestController
#Validated
public class AramaController {
#ResponseStatus(value = HttpStatus.OK)
#GetMapping("/arama")
public List<Arama> arama(#RequestParam #NotEmpty #Size(min = 4, max = 20) String query) {
return aramaService.arama(query);
}
}
This controller works as expected.
curl with no "query" parameter returns Bad Request 400 :
~$ curl http://localhost:8080/arama -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Wed, 12 Dec 2018 21:47:11 GMT
< Connection: close
<
* Closing connection 0
curl with "query=a" as parameter returns Bad Request 400 as well :
~$ curl http://localhost:8080/arama?query=a -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama?query=a HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 12 Dec 2018 21:47:33 GMT
< Connection: close
<
* Closing connection 0
{"message":"Input error","details":["size must be between 4 and 20"]}
This controller and validation works flawlessly when running on a server.
During unit tests the #Validated annotation doesn't seem to have any effect.
Here my test code :
#ExtendWith(MockitoExtension.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
private MockMvc mockMvc;
#BeforeEach
private void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(aramaCcontroller)
.setControllerAdvice(new RestResponseEntityExceptionHandler())
.build();
}
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}
This test results in failure :
java.lang.AssertionError: Status expected:<400> but was:<200>
Expected :400
Actual :200
Since the #Valid annotations pass my other test cases, and they work without loading the Spring context, is there a way to make the #Validated annotation work as well with Mockito (again, without loading the Spring context) ?

I got the answer elsewhere and wanted to share :
Without starting up the context, you won't have #Validator getting
tested because validator instances are Spring beans. However, #Valid
will work as it is a JSR-303 standard.
As of now, what I can suggest is.
#SpringBootTest
#ExtendWith(SpringExtension.class)

maybe you can try using #WebMvcTest and add SpringExtension
#ExtendWith({SpringExtension.class, MockitoExtension.class})
#WebMvcTest(AramaController.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
#Autowired
private MockMvc mockMvc;
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}

Related

Spring webflux: ServerResponse redirection

This is my related code:
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ServerResponse> gicar(#RequestHeader("GICAR_ID") String gicarId) {
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
Issue arises when I'm calling to _/login endpoint:
$ curl -i -X POST localhost:8080/login -H "GICAR_ID: tre"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
curl: (18) transfer closed with outstanding read data remaining
Why am I getting an 200 http code response?
On spring boot logging I'm getting this exception:
022-06-27 13:11:19.931 ERROR 79654 --- [or-http-epoll-2] r.n.http.server.HttpServerOperations : [9750a9d8-1, L:/127.0.0.1:8080 - R:/127.0.0.1:33150] Error finishing response. Closing connection
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Why above exception is reaised?
Any ideas?
According to Spring documentation ServerResponse
Represents a typed server-side HTTP response, as returned by a handler function or filter function.
and it supposed to be used in Functional Endpoints
#Configuration
public class GicarConfiguration {
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions
.route(POST("/login"), this::loginHandler);
}
private Mono<ServerResponse> loginHandler(ServerRequest request) {
var gicarId = request.headers().firstHeader("GICAR_ID");
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
If you still want to use Annotated Controllers, use ResponseEntity instead
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ResponseEntity<Void>> gicar() {
return Mono.just(ResponseEntity
.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "/me")
.build()
);
}
}

#PreAuthorize makes validation not working for primitive type

First of all, this is my development environent.
Spring boot: org.springframework.boot:2.5.6
io.spring.dependency-management: 1.0.11.RELEASE
Spring Security: org.springframework.boot:spring-boot-starter-security
org.springframework.boot:spring-boot-starter-webflux
kotlin
org.springframework.boot:spring-boot-starter-validation
When I call this api with invalid value of limit(as 100), validator(#Max) is working successfuly.
Call: GET {{apiEndpoint}}/workspaces/{{workspaceId}}/test?limit=1000
Code
#GetMapping("/test")
#ResponseStatus(HttpStatus.OK)
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestParam(name = "limit", defaultValue = "15") #Max(20) limit: Int
): String {
return "OK"
}
Response
HTTP/1.1 500 Internal Server Error
requestId: 32511EB3433F4D1DBEAC56641E6BE1A2
Content-Type: application/json
Content-Length: 213
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
{
"requestId": "32511EB3433F4D1DBEAC56641E6BE1A2",
"message": "test.limit: 20 이하여야 합니다",
"sys": {
"id": "UnhandledError",
"code": "50001",
"type": "Error"
},
"details": {
"exception": "ConstraintViolationException"
}
}
But with #PreAutorize annotation, validator is not working.
Code
#GetMapping("/test")
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('USER')")
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestParam(name = "limit", defaultValue = "15") #Max(20) limit: Int
): String {
return "OK"
}
Response
HTTP/1.1 200 OK
requestId: B0F832B0AACA49DDB2F5774FE3BFB8D4
Content-Type: application/json;charset=utf8
Content-Length: 2
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
OK
Why spring-security makes validator not working??
Plus, validator for Object(not primitive type) type is working successfuly with #PreAuthorize.
#PostMapping("/test")
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('USER')")
suspend fun test(
auth: AuthToken,
#PathVariable workspaceId: String,
#RequestBody #Valid body: Payload // It works!!
): String {
return "OK"
}
data class Payload(
#field:Max(10)
val age: Int
)
Please help me.
I also tried to use `groups(ValidationGroups)', but it's not works too.

SpringBoot controller redirection doesn't work

Hitting the / directory of my Rest app doesn't redirect to what I what, but just print the redirection directive on the screen: "redirect:swagger-ui.html"
My controller:
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
#RestController
class HomeController(val info: InfoProperties) {
#RequestMapping("/")
fun home(): String {
return "redirect:/swagger-ui.html"
}
}
Using curl, we see that the answer is text (Content-Type: text/plain;charset=UTF-8), hence the simple unexpected text output:
> curl -v "http://localhost:8080/"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: * / *
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 25
< Date: Fri, 15 Mar 2019 17:52:32 GMT
<
* Connection #0 to host localhost left intact
redirect:/swagger-ui.html
The #RestController annotation is a specialized version of the controller. It includes the #Controller and #ResponseBody annotations, and #ResponseBody is the cause of our problem.
So to fix this, replace the #RestController annotation with the more generic #Controller one:
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.stereotype.Controller
#Controller
class HomeController(val info: InfoProperties) {
#RequestMapping("/")
fun home(): String {
return "redirect:/swagger-ui.html"
}
}
The redirection now works properly.
You can actually achieve this without changing #RestController to #Controller. What you need to do is return a RedirectView instead of a string. This is how I have it working in java:
#RestController
#ApiIgnore
public class ApiDocsRedirectController {
#RequestMapping(value = {"/","/api-docs","/v3/api-docs"})
public RedirectView redirect() {
return new RedirectView("/swagger-ui.html");
}
}

RequestMapping GET and POST methods handling in Spring REST

I have such code:
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#RequestMapping(value = "/foo", method = RequestMethod.POST)
String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
return param1 + param2;
}
#RequestMapping("/foo")
String testGet(#RequestParam String param1) {
return param1;
}
}
And I execute such curl expressions:
curl --verbose http://localhost:9000/foo?param1=Sergei
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> GET /foo?param1=Sergei HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 404 Not Found
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 08:55:56 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379356514,"status":404,"error":"Not Found","message":"No message available","path":"/foo"}
and,
curl --verbose --data "param1=value1&param2=value2" http://localhost:9000/foo
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST /foo HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 out of 27 bytes
< HTTP/1.1 405 Method Not Allowed
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Allow: HEAD, GET
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 09:01:46 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379706832,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/foo"}
Help me please get both my methods work.
Add a RestController or Controller stereotype annotation to your Application class like this:
#RestController
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
...
}
Note: You can use SpringBootApplication meta annotation instead of those three, so you would have:
#RestController
#SpringBootApplication
public class Application {
....
}
You should change the scope level of the two controller methods to public. Right now, they don't have any, so the methods are package local by default.
public String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
public String testGet(#RequestParam String param1) {

WebSocket with Spring backend loses connection after a while, onclose is not called

In our spring application most of the controllers are protected with oauth security. Websockets are behind basic. Before accessing websocket logged user asks for username and hashed password for websocket connection. Both are going to be generated, but for now for testing purposes it always returns the same creditentials.
URL for info looks as follows:
https://user:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/info
WebSocket is opened properly. We can send few messages and they go trough broker and are displayed to the users. Here's request info from chrome tools:
Remote Address:127.0.0.1:8000
Request URL:https://benny:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/033/7szz8k_f/xhr_send
Request Method:POST
Status Code:204 No Content
Response Headers:
HTTP/1.1 204 No Content
server: Apache-Coyote/1.1
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
access-control-allow-origin: https://localhost:8000
access-control-allow-credentials: true
vary: Origin
content-type: text/plain;charset=UTF-8
date: Mon, 15 Jun 2015 08:22:43 GMT
Connection: keep-alive
Request Headers:
POST /project-name/chat/033/7szz8k_f/xhr_send HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 143
Origin: https://localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: https://localhost:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,pl;q=0.6
Cookie: JSESSIONID=FF967D3DD1247C1D572C15CF8A3D5E8E; i18next=en; language=pl; tmhDynamicLocale.locale=%22pl-pl%22
["SEND\npriority:9\ndestination:/random/chat/1/FUNNY\ncontent-length:49\n\n{\"message\":\"sfsdf\",\"display\":\"The great wizard.\"}\u0000"]
But after a minute or so when sending another request we get 404 response. It doesn't matter if any SEND requests were issued before. We can write 50+ messages in that time span and then we get 404.
Sample 404 request data follows:
Remote Address:127.0.0.1:8000
Request URL:https://hill:debaee4affbeaba909a184066981d55a#localhost:8000/project-name/chat/033/7szz8k_f/xhr_send
Request Method:POST
Status Code:404 Not Found
Response Headers:
HTTP/1.1 404 Not Found
server: Apache-Coyote/1.1
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
content-length: 0
date: Mon, 15 Jun 2015 08:24:17 GMT
Connection: keep-alive
Request Headers:
POST /project-name/chat/033/7szz8k_f/xhr_send HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 143
Origin: https://localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: https://localhost:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,pl;q=0.6
Cookie: JSESSIONID=FF967D3DD1247C1D572C15CF8A3D5E8E; i18next=en; language=pl; tmhDynamicLocale.locale=%22pl-pl%22
Request Payload:
["SEND\npriority:9\ndestination:/random/chat/1/FUNNY\ncontent-length:49\n\n{\"message\":\"yhgfh\",\"username\":\"The great wizard.\"}\u0000"]
When setting up stomp we setup function to react onclose:
socket.client = new SockJS(targetUrl);
socket.stomp = Stomp.over(socket.client);
socket.stomp.connect({}, startListener);
socket.stomp.onclose = reconnect;
With reconnect function looking as this(it's in AngularJS):
var reconnect = function() {
$log.debug('Reconnect called');
$timeout(function() {
initialize();
}, this.RECONNECT_TIMEOUT);
};
But the function is never called.
Controller for Chat is pretty simple:
#Controller
public class StageChatController {
#Inject
private SimpMessagingTemplate template;
#Inject
private ChatMessageRepository chatMessageRepository;
#MessageMapping("/chat/{channel}/{type}")
public void sendMessage(#DestinationVariable Long channel, #DestinationVariable ChatType type, ChatMessageDto message) {
ChatMessage chatMessage = new ChatMessage();
chatMessage.setDatestamp(LocalDateTime.now());
chatMessage.setMessage(message.getMessage());
chatMessage.setChannelId(channel);
chatMessage.setChatType(type);
chatMessage.setDisplayName(message.getDisplay());
chatMessage = this.chatMessageRepository.save(chatMessage);
this.template.convertAndSend("/channel/" + project + "/" + type, chatMessage);
}
Security for chat overrides oauth security for chat urls:
#Configuration
#EnableWebSecurity
#Order(2)
static class BasicAccessConfig extends WebSecurityConfigurerAdapter {
#Inject
private OAuth2ClientContextFilter oauth2ClientContextFilter;
#Value("${project.name.chat.token}")
private String chat_token;
#Override
protected void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.requestMatcher(new AntPathRequestMatcher("/chat/**/*"))
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic()
.and()
.anonymous().disable()
.csrf().disable()
.addFilterBefore(this.oauth2ClientContextFilter, SecurityContextPersistenceFilter.class);
;
//#formatter:on
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("hill").password(this.chat_token).authorities("read_chat");
}
}

Resources