Given the following classes :
class Contact{
#Getter
#Setter
#Email
private String mail;
}
#RestController
class ContactController{
#PostMapping(path = "/contacts")
public ResponseEntity<Contact> addContact(
#Valid #RequestBody Contact contact) {
....
}
}
How can I test in a gherkin scenario ?
I've tried the following :
Scenario Outline: validation failed use-case
Given I am authenticated as "<userFirstName>"
When I register a new contact "<contactJsonLocation>"
Then it should throw an ConstraintViolationException of contact
Examples:
| userFirstName | contactJsonLocation |
| Simon | contactWithMailNotWellFormatted.json |
Steps code :
public CreateContactSteps(ContactController contactController) {
When("^I register a new contact \"([^\"]*)\"$",
(String contactJsonLocation) -> {
try {
Contact contact;
String jsonContact = Files.readString(Paths.get(new ClassPathResource("files/" + contactJsonLocation).getURI()));
contact = objectMapper.readValue(jsonContact, Contact.class);
String id = UUID.randomUUID().toString();
contact.setId(id);
createContactAttempt.setId(id);
createContactAttempt.setMail(contact.getMail());
contactController.addContact(contact);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ConstraintViolationException e) {
createContactAttempt.lastException = e;
}
});
Then("^it should throw an ConstraintViolationException of contact$",
() -> {
Assertions.assertTrue(createContactAttempt.lastException instanceof ConstraintViolationException);
createContactAttempt.lastException = null;
});
When I'm testing to create a new contact after starting my spring boot app all is going well on the validation part, meaning I'm receiving the expected 400 error.
But when I'm calling the ContactController from my test context, it fails to valid and the contact is created.
I'm guessing it has something to do with spring doing some magic behind the scene, but what ?
Right now I'm initiating myself the cucumber application context like this (and I might be doing something wrong, I'm open to suggestion / good criticism) :
#RunWith(Cucumber.class)
#CucumberOptions(plugin = {"pretty", "html:FeaturesReport.html"},
features = {"src/test/resources/features"})
public class AllAcceptanceTest {
}
#CucumberContextConfiguration
#ContextConfiguration(classes = {BeanConfiguration.class})
public class ContextConfigurationTesting implements En {
}
#Configuration
#RequiredArgsConstructor
public class ControllerConfiguration {
#Bean
public ContactController contactController() {
return new ContactController(
... //every other bean the controller need
);
}
...
}
The #Valid annotation signals the caller of addContact that the object should be valid. By directly calling addContact you are bypassing that. You may want to consider using MockMvc to call your controller instead.
I would suggest using Spring boot to setup your application context instead. Spring Boot has a rich set of features that let you test many aspects of your application easily.
Related
I'm making a Reddit clone as one of the projects for my portfolio.
The problem I'm unable to solve (I'm a beginner) is this:
I have a CommentController (REST) that's handling all the api calls regarding comments. There's an endpoint for creating a comment:
#PostMapping
public ResponseEntity<Comment> createComment(#Valid #RequestBody CommentDto commentDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw new DtoValidationException(bindingResult.getAllErrors());
URI uri = URI.create(ServletUriComponentsBuilder.fromCurrentContextPath().path("/api/comments/").toUriString());
Comment comment = commentService.save(commentDto);
return ResponseEntity.created(uri).body(comment);
}
In my CommentService class, this is the method for saving a comment made by currently logged in user:
#Override
public Comment save(CommentDto commentDto) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Optional<User> userOptional = userRepository.findUserByUsername((String) principal);
if (userOptional.isPresent()) {
User user = userOptional.get();
Optional<Post> postOptional = postRepository.findById(commentDto.getPostId());
if (postOptional.isPresent()) {
Post post = postOptional.get();
Comment comment = new Comment(user, commentDto.getText(), post);
user.getComments().add(comment);
post.getComments().add(comment);
post.setCommentsCounter(post.getCommentsCounter());
return comment;
} else {
throw new PostNotFoundException(commentDto.getPostId());
}
} else {
throw new UserNotFoundException((String) principal);
}
}
The app is running normally with no exceptions, and comment is saved to the database.
I'm writing an integration test for that controller, I used #WithMockUser(username = "janedoe", password = "password") on a test class, and I kept getting this exception:
ClassCastException: UserDetails can not be converted to String
I realized that the problem is with this two lines in save method:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Optional<User> userOptional = userRepository.findUserByUsername((String) principal);
What I don't get is why are those to lines throwing exception only in tests. When the app is running, everything is okay.
I guess that for some reason in tests the .getPrincipal() method is not returning a String (username), but the whole UserDetails object. I'm not sure how to make it return username in tests.
What have I tried to solve it:
Changing #WithMockUser to #WithUserDetails
Using both #WithMockUser and #WithUserDetails on both class- and method-level
Creating custom #WithMockCustomUser annotation:
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public #interface WithMockCustomUser {
String username() default "janedoe";
String principal() default "janedoe";
}
Which just gives me the same ClassCastException with different text:
class com.reddit.controller.CustomUserDetails cannot be cast to class java.lang.String (com.reddit.controller.CustomUserDetails is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
Any help is appreciated :)
You right. It is because the authentication logic in your production codes are different from the test. In production codes it configure a string type principal to the Authentication while #WithMockUser / #WithUserDetails configure a non-string type principal.
Implement a custom #WithMockCustomUser should work as long as you configure a string type principal to Authentication.
The following implementation should solve your problem :
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public #interface WithMockCustomUser {
String[] authorities() default {};
String principal() default "foo-principal";
}
public class WithMockCustomUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockCustomUser> {
#Override
public SecurityContext createSecurityContext(WithMockCustomUser withUser) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : withUser.authorities()) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(withUser.principal(),
"somePassword", grantedAuthorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
And use it in your test :
#Test
#WithMockCustomUser(principal="janedoe")
public void test() {
// your test code
}
I am writing my end-to-end tests for an application. One of those tests will verify that when some required input is missing, it will not persist the data to the database. All is running fine, untill I decided to annotate my test class with #Transactional, since I don't need to persist the results when the tests are finished. The moment I add the #Transactionalannotation, the application is suddenly ok with a non-null field to be null.
How the database is set up:
create table MY_TABLE
(
MY_FIELD VARCHAR(20) NOT NULL
)
The corresponding model:
#Entity
#Table(name = "MY_TABLE")
public class MyObject {
#Column(name = "MY_FIELD", nullable = false)
private String MyField = null;
}
Saving goes via the service layer:
#Service
public class MyAppServiceImpl implements MyAppService {
#Inject
private MyObjectRepository myObjectRepository; //this interface extends CrudRepository<MyObject , Long>
#Override
#Transactional
public MyObject save(MyObject myObject) {
return myObjectRepository.save(myObject);
}
}
In the resource layer, the value of myField is set to NULL, so at the moment the data is persisted, I should receive an ORA-error
#Controller
public class MyAppResourceImpl implements MyAppResource {
#Inject
private MyAppService myAppService;
public ResponseEntity doPost(#RequestBody String xml) {
MyObject myobject = new MyObject();
myObject.setMyField(null);
try {
myAppService.save(myObject);
return new ResponseEntity(null, HttpStatus.CREATED);
} catch (SomeExceptions e) {
return new ResponseEntity("Could not save", HttpStatus.BAD_REQUEST);
}
}
}
Finally, the test:
#RunWith(SpringRunner.class)
#SpringBootTest
//#Transactional
public class MyAppResourceImplEndToEndTest {
#Inject
private MyAppResourceImpl myAppResource;
#Test
public void testWithFieldNull() throws Exception {
ResponseEntity response = myAppResource.doPost("some-xml");
Assertions.assertThat(response.GetStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
}
This test works, as expected, but other tests persist data into the database. This is not the behavior I wanted, so I annotated the test class with #Transactional. Now no tests persist their results to the database, but this specific test method fails because
Expected :400
Actual :201
Why does the #Transactional annotation suddenly cause NULL values to be persisted in NOT NULL columns?
I have a controller's method with a getMapping and a pathValue:
#ResponseBody
#Validated
#GetMapping(produces = [MediaType.APPLICATION_JSON_UTF8_VALUE],
value = '/{person}')
PersonResponse getPersonInfo (
#Valid #PersonValidation
#PathVariable('person') String personId,
BindingResult bindingResult
) {
// Abort if received data is not valid
if (bindingResult.hasErrors()) {
throw new BadHttpRequest()
}
What I am trying to achieve is validating the uri's personId using my custom validation #PersonValidation but it is not working, the code is not giving any errors but the program never goes into my custom validation class...
¿How can I fix this?
¿How can I validate my path variable with a custom validation?
I have solved my problem. Spring have some problems to validate path variables...
First, you need to add a configuration class with a MethodValidationPostProcessor like this one:
/**
* this class is required to use our custom path variable validation on
* the controller class
*/
#Configuration
class ValidatorConfiguration {
#Bean
MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor()
}
}
Now, on the controller class, it is important to add #Validated annotation
#RestController
#Validated
class YourController class {
}
now your custom validation on the path variable is gonna work, if you want to catch the exception, add this method inside a exception handler class:
#ControllerAdvice
class GlobalExceptionHandler {
#ExceptionHandler(value = [ ConstraintViolationException.class ])
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
String handle(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations()
StringBuilder strBuilder = new StringBuilder()
for (ConstraintViolation<?> violation : violations ) {
strBuilder.append(violation.getMessage() + "\n")
}
return strBuilder.toString()
}
}
So I have an API client type class right now, which I am trying to connect to my repository so that I can store data in the MySQL database.
The problem I'm having is that the API client class instantiates a new object of itself, so the Autowiring doesn't work correctly. I've looked around for a workaround for this problem, and I've seen a couple options, but I'm confused on how to apply them to my problem.
For reference, here are parts of some of the relevant files:
GeniusApiClient.java:
#Component
public final class GeniusApiClient {
private final OkHttpClient client = new OkHttpClient();
#Autowired
private ArtistDao artistDao;
public static void main(String[] args) throws Exception {
GeniusApiClient geniusApiClient = new GeniusApiClient();
String artistId = (geniusApiClient.getArtistId("Ugly Duckling"));
ArrayList<String> artistSongIds = geniusApiClient.getArtistSongIds(artistId);
System.out.println(geniusApiClient.getAllSongAnnotations(artistSongIds, artistId));
}
public String getAllSongAnnotations(ArrayList<String> songIds, String artistId) {
Artist artist = new Artist("test name for now", "string123", "223");
artistDao.save(artist);
return "finished";
}
}
ArtistDao.java:
#Transactional
public interface ArtistDao extends CrudRepository<Artist, Long> {
public Artist findByGeniusId(String geniusId);
}
ArtistController.java:
#Controller
public class ArtistController {
#Autowired
private ArtistDao artistDao;
/**
* GET /create --> Create a new artist and save it in the database.
*/
#RequestMapping("/create")
#ResponseBody
public String create(String name, String annotations, String genius_id) {
String userId = "";
try {
genius_id = genius_id.replaceAll("/$", "");
Artist artist = new Artist(name, annotations, genius_id);
artistDao.save(artist);
userId = String.valueOf(artist.getId());
}
catch (Exception ex) {
return "Error creating the artist: " + ex.toString();
}
return "User succesfully created with id = " + userId;
}
/**
* GET /get-by-email --> Return the id for the user having the passed
* email.
*/
#RequestMapping("/get")
#ResponseBody
public String getByEmail(String genius_id) {
String artistId = "";
try {
Artist artist = artistDao.findByGeniusId(genius_id);
artistId = String.valueOf(artist.getId());
}
catch (Exception ex) {
return "User not found";
}
return "The user id is: " + artistId;
}
}
The problem is that in GeniusApiClient.java in the getAllSongAnnotations method, I have a null pointer exception when I try and access the artistDao. I understand that my instantiation of this class is what is messing up the Autowiring, but I'm curious on what the best way to go about fixing this might be.
I considered making all of my methods in the class static so that I wouldn't have to instantiate a new method, but I don't think this would work very well. Any suggestions?
Thanks
EDIT:
Removed some irrelevant code for clarity.
EDIT2:
Added ArtistController.java
To be able to autowire/inject an object, that object must be a Spring bean.
Here you can't autowire ArtistDao because it's not a bean. There are several annotation options to make it bean but the one suits in this case is #Repository annotation. It's just a specialized version of #Component which you used in GeniusApiClient class.
So,
#Repository
#Transactional
public interface ArtistDao extends CrudRepository<Artist, Long> {
public Artist findByGeniusId(String geniusId);
}
should work.
I'd suggest you to read: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html
If reading reference documentation sounds scary to you, you can also take a look at Core Spring part of Spring in Action.
Don't make GeniusApiClient.class final. Spring will use CGLIB to dynamically extend your class in order to make a proxy. And the requirement for CGLIB to work is to have your classes non-final.
More on this here: Make Spring Service Classes Final?
What you are trying to do in your catch block is not clear to me,you have to correct that and replace it with desired action to be taken on any exception occurrence.
I need to show custom messages in my Spring 3.0 application. I have a database with Hibernate and there are several constraints. I have doubts in how DataIntegrityViolationException should be handled in a good way. I wonder if there is a way to map the exception with a message set in a properties file, as it is possible in Constraints validation. Could I handle it automatically in any way or I have to catch this exception in each controller?
The problem with showing user-friendly messages in the case of constraint violation is that the constraint name is lost when Hibernate's ConstraintViolationException is being translated into Spring's DataIntegrityViolationException.
However, you can customize this translation logic. If you use LocalSessionFactoryBean to access Hibernate, you can supply it with a custom SQLExceptionTranslator (see LocalSessionFactoryBean.jdbcExceptionTranslator). This exception translator can translate a ConstraintViolationException into your own exception class, preserving the constraint name.
I treat DataIntegrityViolationException in ExceptionInfoHandler, finding DB constraints occurrences in root cause message and convert it into i18n message via constraintCodeMap:
#RestControllerAdvice
public class ExceptionInfoHandler {
#Autowired
private final MessageSourceAccessor messageSourceAccessor;
private static Map<String, String> CONSTRAINS_I18N_MAP = Map.of(
"users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL,
"meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME);
#ResponseStatus(value = HttpStatus.CONFLICT) // 409
#ExceptionHandler(DataIntegrityViolationException.class)
public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
String rootMsg = ValidationUtil.getRootCause(e).getMessage();
if (rootMsg != null) {
String lowerCaseMsg = rootMsg.toLowerCase();
for (Map.Entry<String, String> entry : CONSTRAINS_I18N_MAP.entrySet()) {
if (lowerCaseMsg.contains(entry.getKey())) {
return logAndGetErrorInfo(req, e, VALIDATION_ERROR, messageSourceAccessor.getMessage(entry.getValue()));
}
}
}
return logAndGetErrorInfo(req, e, DATA_ERROR);
}
...
}
Can be simulated in my Java Enterprise training application by adding/editing user with duplicate mail or meal with duplicate dateTime.
UPDATE:
Other solution: use Controller Based Exception Handling:
#RestController
#RequestMapping("/ajax/admin/users")
public class AdminAjaxController {
#ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorInfo> duplicateEmailException(HttpServletRequest req, DataIntegrityViolationException e) {
return exceptionInfoHandler.getErrorInfoResponseEntity(req, e, EXCEPTION_DUPLICATE_EMAIL, HttpStatus.CONFLICT);
}
Spring 3 provides two ways of handling this - HandlerExceptionResolver in your beans.xml, or #ExceptionHandler in your controller. They both do the same thing - they turn the exception into a view to render.
Both are documented here.
1. In your request body class check for not null or not empty like this
public class CustomerRegisterRequestDto {
#NotEmpty(message = "first name is empty")
#NotNull(message = Constants.EntityValidators.FIRST_NAME_NULL)
private String first_name;
//other fields
//getters and setters
}
2. Then in your service check for this
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<CustomerRegisterRequestDto>> violations = validator.validate(userDto);
if (!violations.isEmpty()) {
//something is wrong in request parameters
List<String> details = new ArrayList<>();
for (ConstraintViolation<CustomerRegisterRequestDto> violation : violations) {
details.add(violation.getMessage());
}
ErrorResponse error = new ErrorResponse(Constants.ErrorResponse.REQUEST_PARAM_ERROR, details);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
3. Here is your ErrorResponse class