Assignment Test

Info

Create 2 REST services: one that allows the registration of a user and the other one that displays the details of a registered user.

Requirements:

  • define a user (what are the fields needed). We should have mandatory and optional fields!
  • validate the input and return proper error messages/HTTP status
  • log the input and output of each call and the processing time.
  • have a request parameter that is not mandatory and which provides a default value in case it is not set
  • have a path variable
  • clear code and Javadoc
  • unit tests
  • only adults ( age > 18 years) and those who live in France can create an account!

Bonuses:

  • user a non-relational DB to save the users!
  • use AOP
  • documentation/UML/schemas to explain the architecture

Prerequisites

  • Java v11
  • Spring Boot v2.4.5
  • Maven Project
  • Maven Build Tool
  • Lombok (Additional Library)

Installation

git clone https://github.com/BardisRenos/Rest-Api-with-Spring-Boot-Test.git

To clean and install the Maven repository dependencies.

mvn clean install

Application Properties

We are changing the server port from 8080 (Default) to 8081.

server.port=8081

Setting the non-relational database

Setting the MongoDB in-memory database.

#Mongo Configuration
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mongodb
spring.data.mongodb.repositories.enable=true
logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG

Data Model

The Model part represents the database table schema in Java coding. The Setting of the attributes of the User object which corresponds with the Users database table schema attributes.

The @Document corresponds to the database table name. Also, the @Data is the annotation that helps a developer to interact with the database's entities.

@Data
@Document(collection = "user")

Setting the mongodb service

If you work on Linux operation system. The command to check the status of MongoDB server is on :

sudo systemctl status mongod

To start the service if it is down

sudo systemctl start mongod

By using the annotation @Id sets the primary key (Id attribute) which will be unique, nullable and not updatable.

@Id
private String id;

By using the annotations @NotNUll and @Size you specify that the entity should not be null and also the size should be inside of a specific size.

@NotNull(message = "The name cannot be null")
@Size(min = 5, max = 32, message = "The name cannot be less than 5 and greater than 32 characters")
private String name;

In this case when the city could be null

private String city;

DTO's (Data Transfer Object) and DTO Mappers

The conversation of the database entities into Data transform objects. We try not to expose the entities from the database.

Since we are using Maven, just add the modelmapper library as a dependency.

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>2.4.2</version>
</dependency>

Coding in Java, how to convert an entity to DTO.

ModelMapper modelMapper = new ModelMapper();
OrderDTO orderDTO = modelMapper.map(order, OrderDTO.class);

The two different structures

How architecture should be, with DTO and without it.

3 Layers

With only Entity:

Presentation => REST CONTROLLER
        ENTITY
        |
BUSINESS => SERVICES
        |
        ENTITY
DAL (Data Access Layer) => Repositories

Issues:
    - infinite recursion loop in JSON deserialisation
    - Business Logic is tied to the definition of the model
        (EVERYTHING HAS TO BE IN DATABASE, which is not always the case)

With DTO:

Presentation => REST CONTROLLER------------------
    DTO                                 /\
    |                                   |
    v                                    DTO
BUSINESS => SERVICES---------------------------
    DTO transform to ENTITY             ENTITY trasnform to DTO
    |                                   /\
    v                                   |
    Save ENTITY                        GET ENTITY
DAL (Data Access Layer) => Repositories--------

Non Issue:
    A bit more code to do (sometimes it can be just a copy of an entity)
++:
    More flexible in the Business Lyaer to add data
    Separate

Validation

To validate the attributes of a User object, should use @Valid as a parameter.

@PostMapping("/user")
public UserDTO saveUser(@Valid @RequestBody User user) throws UserValidationException 

Insert values into the database

When the application starts, the database has already 4 records, in order to check if the application works.

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		ApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	public CommandLineRunner initDB(UserRepository userRepository){
		return (args) ->{
			List<User> users = new ArrayList<>(Arrays.asList(new User(1, "Renos", "Bardis", 20, "78 BD DD", "Antes", "France"),
					new User(2, "Nikos", "Papas", 40, "10 BVD LL", "Lyon", "France"),
					new User(3, "Aggelos", "MPalallis", 30, "90 BVD MM", "Paris", "France"),
					new User(4, "Vaggelis", "Papagkikas", 50, "90 RUE LL", "Bordeaux", "France")));

			userRepository.saveAll(users);
		};

User Exception Handling

@ExceptionHandler({UserValidationException.class})
public ResponseEntity<Object> handleBadRequest(Exception ex) {
    return new ResponseEntity<Object>(ex.getMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler({UserNotFoundException.class})
public ResponseEntity<Object> handleNotFound(Exception ex) {
    return new ResponseEntity<Object>(ex.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND);
}

User Not Found Exception

The 404 which is the NOT_FOUND status

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends Exception

UserValidation Exception

The 400 which is BAD_REQUEST status

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class UserValidationException extends Exception 

User Validator

The validator class checks if a given User is greater than 18 and if he is from France. Otherwise, the method validate throws a UserValidationException type.

@Component
public class UserValidator {

    final String COUNTRY = "France";

    public void validate(User user) throws UserValidationException {
        if (user.getAge() <= 18 || !COUNTRY.equals(user.getCountry())) {
            throw new UserValidationException(String.format("User must be older than 18 years old and live in %s", COUNTRY));
        }
    }
}

Docker

It is possible to deploy and run the application via docker container.

FROM openjdk:11
EXPOSE 8088
ADD target/spring-app-docker.jar spring-app-docker.jar
ENTRYPOINT ["java", "-jar", "/spring-app-docker.jar"]

To build the docker image. This command is needed.

docker build -t spring-boot-application-docker.jar .

Moreover, to run the container into a specific port, this commands is needed.

docker run -p 9090:8081 spring-boot-application-docker.jar

JUnit Testing

It is best to test all the layers of this application.

  • Repository layer
  • Service layer
  • Controll layer

For the repository layer:

  @BeforeEach
  public void init(){
      User user = new User(1, "Renos", "Bardis", 20, "78 BD DD", "Antes", "France");
      User user1 = new User(2, "Nikos", "Papas", 40, "10 BVD LL", "Lyon", "France");
      userRepository.save(user);
      userRepository.save(user1);
    }

With the annotation @BeforeEach before each @Test part, the method insert two user object.

  @AfterEach
  public void delete(){
      userRepository.deleteAll();
    }

With the annotation @AfterEach after each @Test part, the method delete all the records.

In this case the method testData() retrieve all the records and checks if the total number of records are 2 and the first record the name of the user is Renos.

@Test
public void testData(){
    List<User> userRes = (List<User>) userRepository.findAll();
    assertEquals(2, userRes.size());
    assertEquals("Renos", userRes.get(0).getName());
  }

In the other case is the riverce side of the first unit test. Retrieve all the records and checks if the name is not we gave and the total size of the list not 1.

@Test
public void testDataNot(){
    List<User> userRes = (List<User>) userRepository.findAll();
    assertNotEquals("George", userRes.get(0).getName());
    assertNotEquals(1, userRes.size());
}

For the service layer:

In this layer, Mockito testing will be used. In this case mock the findById() method which returns an Optional User. Followed by the service method getUser(). Checks the name and the last name.

@Test
void getUser() throws UserNotFoundException {
    when(userRepository.findById(1)).thenReturn(Optional.of(new User(1, "Renos", "Bardis", 20, "78 BD DD", "Antes", "France")));
    UserDTO userRes = userService.getUser(1);
    assertEquals("Renos", userRes.getName());
    assertEquals("Bardis", userRes.getLastName());
}

For the controller layer :

In this test case the method getUser() return a UserDTO type. Then mockMvc apply the path of the controller /user/{id} and chekcs if the HttpStatus is ok, namely 200. Also, checks the id, name and the last name.

@Test
  public void getUserById() throws Exception {
      final int id = 1;
      UserDTO userDTO = new UserDTO(1, "Nikos", "Papas", 40, "10 BVD LL", "Lyon", "France");

      when(userService.getUser(id)).thenReturn(userDTO);

      MvcResult mvcResult = mockMvc.perform(get("/user/{id}", id)
              .contentType(MediaType.APPLICATION_JSON)
              .content(om.writeValueAsString(userDTO)))
              .andExpect(status().isOk())
              .andExpect(content().contentType(MediaType.APPLICATION_JSON))
              .andExpect(jsonPath("$.id", is(userDTO.getId())))
              .andExpect(jsonPath("$.name", is(userDTO.getName())))
              .andExpect(jsonPath("$.lastName", is(userDTO.getLastName())))
              .andReturn();

      String jsonResponse = mvcResult.getResponse().getContentAsString();
      UserDTO userCreated = new ObjectMapper().readValue(jsonResponse, UserDTO.class);

      Assertions.assertNotNull(userCreated);
      assertEquals(userCreated.getId(), userDTO.getId());
      assertEquals(userCreated.getName(), userDTO.getName());
      assertEquals(userCreated.getAge(), userDTO.getAge());
  }

Testing screenshot

From this screen shot it is obvious that all the test are passed.

Run All test with Coverage

UML Diagram

In this section will demonstrate the UML diagram.

Flow diagram of the structure

This structure depicts the flow of each component until the database.

UML Sequence Diagram