¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de realizar la implementación de la capa de API junto con las pruebas de integración en Postman.

¿Qué necesita?

Para realizar este taller Ud. debe:

  1. Aprovisionar su máquina virtual o realizar el tutorial de máquina propia.
  2. Contar con el modelo de entidades
  3. Tener clonado el proyecto back y haber realizado el tutorial de Persistencia y Lógica.

El proyecto del curso está dividido en varias capas. En este tutorial abordaremos la capa correspondiente a los servicios REST. Esta capa proporciona una forma de comunicación entre el front-end y el back-end de la aplicación.

Como observamos en el diagrama, la interfaz de usuario (UI) y Postman son clientes o consumen el API REST. Esto se logra a través del protocolo HTTP que mediante el uso de algunos de sus métodos (GET, POST, UPDATE y DELETE) se realizan las operaciones sobre los recursos.

El proyecto, en su vista de desarrollo está organizado en varios paquetes. El paquete controllers incluye las clases que representan los endpoints para el API REST. El paquete services contiene las clases encargadas de la lógica. Finalmente, el paquete repositories contiene las clases encargadas de la persistencia.

En pasados tutoriales abordamos los paquetes services y repositories. En este tutorial nos enfocaremos en los paquetes controller y dto.

El siguiente diagrama presenta las clases involucradas en la implementación de un controlador de un recurso. En este caso nos centraremos en el recurso Book.

En el diagrama tenemos dentro del paquete de controladores la clase BookController que depende de las clases que definen la representación de los recursos BookDTO y BookDetailDTO. La clase BookController se comunica con la lógica de la aplicación a través de un atributo llamado bookService que es una instancia de BookService. En lo que sigue revisaremos en más detalle estos elementos.

(Para ver mejor la imagen, ábrala en una nueva pestaña en el navegador)

Una decisión de diseño muy importante que se debe tomar es la representación de los recursos. En esta aplicación, nuestra decisión de diseño para las represnetaciones de los recursos es la siguiente:

  1. Representación básica: Una clase (DTO) que contiene los atributos básicos del recursos y las asociaciones cuya cardinalidad es 1.
  2. Representación detallada: una clase que hereda de la anterior que detalla la información del recurso (DetailDTO) porque contiene las disposiciones de cardinalidad múltiple que son colecciones de representaciones básicas (colecciones de DTOs).

Hay que tener en cuenta que las entidades, (Entity) en la capa de persistencia, son clases que representan al modelo de datos, que se mapean directamente contra tablas de la base de datos. Dicho esto, las entidades son clases que fueron diseñadas para mapear contra la base de datos, no para ser una vista para una pantalla o servicio determinado. Por tanto, se debe crear un objeto plano (también conocido como POJO o DTO) con una serie de atributos que puedan ser enviados o recuperados del servidor.

En primer lugar debemos entender la clase BookDTO.

package co.edu.uniandes.dse.bookstore.dto;

import java.util.Date;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class BookDTO {
        private Long id;
        private String name;
        private String isbn;
        private String image;
        private Date publishingDate;
        private String description;
        private EditorialDTO editorial;
}

Es importante resaltar que estas clases simplemente deben contener los atributos que se quieren incluir en la transferencia del objeto a través de las capas de la aplicación. Si existe algún atributo que queramos persistir, pero no modificar, no es necesario tenerlo en la clase DTO. Como podemos observar, la clase DTO sólo contiene los atributos simples de la entidad y los atributos que representan una asociación de cardinalidad 1, en el ejemplo, el atributo editorial; no se deben incluir las asociaciones de cardinalidad múltiple ya que esto se hace en la clase DetailDTO. También se incluyen las anotaciones @Getter y @Setter para crear automáticamente los métodos de obtención y modificación de los atributos de la clase.

Una vez creada la clase DTO se crea la clase BookDetailDTO que queda de la siguiente manera (esta clase Java también debe ir en el paquete dto):

package co.edu.uniandes.dse.bookstore.dto;

import java.util.ArrayList;
import java.util.List;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class BookDetailDTO extends BookDTO{
        private List<ReviewDTO> reviews = new ArrayList<>();
        private List<AuthorDTO> authors = new ArrayList<>();
}

Como podemos observar, esta clase hereda de BookDTO, y contiene los atributos de asociación de la entidad con cardinalidad Many (es decir, listas). Los atributos solo se declaran ya que la librería Lombok con las anotaciones @Getter y @Setter crea los getters y setters para cada atributo. Fíjense que estamos haciendo un listado de ReviewDTO y de AuthorDTO que también deben estar presentes en el paquete dto.

Siguiendo con la entidad Book, debemos definir el controlador el cual es la clase Java encargada de crear los endpoints de nuestro API. Vaya a la carpeta controllers dentro de src/main/java y localice la clase BookController. Esta clase tiene el siguiente contenido:

package co.edu.uniandes.dse.bookstore.controllers;

import java.util.List;

import org.modelmapper.ModelMapper;
import org.modelmapper.TypeToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import co.edu.uniandes.dse.bookstore.dto.BookDTO;
import co.edu.uniandes.dse.bookstore.dto.BookDetailDTO;
import co.edu.uniandes.dse.bookstore.entities.BookEntity;
import co.edu.uniandes.dse.bookstore.exceptions.EntityNotFoundException;
import co.edu.uniandes.dse.bookstore.exceptions.IllegalOperationException;
import co.edu.uniandes.dse.bookstore.services.BookService;

/**
 * Clase que implementa el recurso "books".
 *
 * @author ISIS2603
 */
@RestController
@RequestMapping("/books")
public class BookController {

        @Autowired
        private BookService bookService;

        @Autowired
        private ModelMapper modelMapper;
...
}

En primer lugar es importante aclarar las anotaciones de la clase que son las siguientes:

  1. @RestController: indica que se trata de una clase que maneja los servicios REST de un recurso. Convierte la clase en un controlador y permite serializar la respuesta de los métodos.
  2. @RequestMapping: indica que se trata de una clase a la cual se accede desde el API con el endpoint dado por parámetro, en este caso, /books.

Dentro de nuestra clase, debemos inyectar dos atributos a través de la anotación @AutoWired:

  1. La lógica de la entidad en la cual estamos trabajando. Para este caso, la clase BookService
  2. El ModelMapper el cual nos permite hacer el mapeo o conversión entre clases. Por ejemplo, en la función findAll, se obtienen todos los libros que llegan como una lista de BookEntity, los cuales debemos convertir a una lista de BookDetailDTO, que es el objeto de transferencia que necesitamos para resolver la petición. Dado que la capa de lógica solo conoce de entidades (Entities) y que el API solo recibe objetos planos (DTO) se requiere hacer una conversión entre estos dos tipos de datos; este es el trabajo del mapper.

Para probar el API usaremos Postman. Para entender cómo están organizadas las pruebas, abra Postman en su máquina e importe las colecciones. Para esto en el menú lateral izquierdo seleccione Collections > y luego Import.

Luego seleccione Folder y escoja la carpeta denominada "collections" que está en la raíz de la carpeta del proyecto back.

Para este tutorial nos centraremos en las colecciones Book Tests y BookAuthors Test

@PostMapping
@ResponseStatus(code = HttpStatus.CREATED)
 public BookDTO create(@RequestBody BookDTO bookDTO) throws IllegalOperationException, EntityNotFoundException {
        BookEntity bookEntity = bookService.createBook(modelMapper.map(bookDTO, BookEntity.class));
        return modelMapper.map(bookEntity, BookDTO.class);
     }

Para crear un nuevo recurso usaremos el método POST del protocolo HTTP. Para esto anotamos el método de la clase con PostMapping. Este método resuelve peticiones POST de la forma:

POST http://localhost:8080/api/books/

El método también se anota con HttpStatus.CREATED para indicar que el recurso se ha creado y enviar el código 201 al cliente.

En este caso, como parte de la petición se incluye en los parámetros un body el cual contiene un objeto con los atributos para crear un nuevo libro. Este objeto se denota con @RequestBody. En este método se llama al servicio.

@RequestBody BookDTO bookDTO

El objeto que viene desde el cliente en formato DTO se debe convertir a Entity para poder invocar la lógica que no conoce de DTOS sino de Entity. Para facilitar la tarea, hacemos uso de una utilidad definida en el framework String llamada el model mapper. El método map recibe un DTO y la clase a donde se quiere hacer la conversión, en este caso BookEntity. Para que la conversión funcione correctamente, debemos verificar que los nombres de los atributos y sus tipos coinciden en ambas clases.

modelMapper.map(bookDTO, BookEntity.class)

Luego invocamos createBook de la clase BookService. Este llamado se encargará de la creación del nuevo libro y retorna la entidad (BookEntity) creada al controlador. De nuevo, debemos hacer la conversión, esta vez en el otro sentido: de Entity a DTO.

El método dispara IllegalOperationException que es la excepci´+on que se puede generar en la lógica (BookSrevice) como consecuencia de alguna violación de las reglas de negocio.

Pruebas para crear un libro

Recuerde que para crear un libro es necesario asociar una editorial que exista previamente. Para esto el primer request que se debe ejecutar es crear una editorial.

Crear una editorial

Cuando se abre el request podrá observar lo siguiente:

Este es un request de tipo POST. La url de la petición es http://{{ip}}:{{puerto}}/api/editorials. Acá es importante tener en cuenta que este request se puede ejecutar de forma manual desde Postman, pero también se puede ejecutar de forma automática desde Jenkins. En ambos entornos la configuración del request, en particular, ip y puerto de la petición cambian. Por esto podrá notar que ip y puerto son dos variables. El valor que se almacena en estas variables está definido en lo que se conoce como un entorno. En este proyecto tenemos los entornos Colecciones Book e IT. Para ejecutar el request asegúrese que el entorno Colecciones Book es el que está seleccionado (esto se ve en la parte superior derecha de Postman).

En el request se observa el cuerpo (body) de la petición. Este body es un documento en formato JSON en el que estarán los atributos necesarios para crear, en este caso, una editorial. Si en el proyecto revisa la clase EditorialDTO notará que tiene dos atributos (id y nombre). No obstante en el cuerpo de la petición solo se pasa el nombre, dado que el id se generará de forma automática por la base de datos.

En la pestaña Tests se encuentra la prueba del request. En este caso la respuesta que devuelve el API se almacena en la variable data. Luego de la variable data se toma el atributo id (que corresponde al id de la nueva editorial creada) y se almacena en una variable global denominada new_id_e. También se verifica que el código HTTP de la respuesta es 201 lo que significa que el objeto se creó sin errores.

Para ejecutar el request debe asegurarse de que el back está corriendo. Cuando se hace clic en el botón Send en Postman obtendrá la siguiente respuesta.

Esto nos indica que se ha creado la nueva editorial con el nombre "Norma"; se le asignado el id 1; y el test de Postman se ha ejecutado correctamente (ver pestaña Test Results).

Este es el detalle del resultado del test, que indica que no se ha generado error en el test y se ha obtenido el código HTTP 201.

Crear un libro con ISBN inválido 1

En este request se crea un libro con un ISBN inválido. Este es un request de tipo POST. La url de la petición es http://{{ip}}:{{puerto}}/api/books.

En el cuerpo de la petición están los atributos necesarios para crear un nuevo libro. Note que en el atributo editorial se setea el id de la editorial (new_id_e) que se creó en el request anterior. También se debe notar que no se está referenciando en atributo isbn que debe hacer parte del libro.

En las reglas de la lógica se estableció que no es posible crear un libro con un ISBN inválido. Por eso se espera que al ejecutar este request se obtenga el código HTTP de respuesta 412 (precondition failed). También se espera que en el cuerpo de la respuesta se obtenga el mensaje "ISBN is not valid".

Al ejecutar el request se obtiene la siguiente respuesta:

Se observa que el API ha respondido con un error (apierror) que tiene tres atributos: el status de la petición (PRECONDITION_FAILED), la marca de tiempo cuando ocurrió el error y el mensaje del error.

A nivel de test se observa que ambos test pasan correctamente porque el libro no se ha creado y el mensaje de error es el esperado.

Crear un libro con ISBN inválido 2

En esta petición se crea un libro con ISBN inválido. Este es un request de tipo POST. La url de la petición eshttp://{{ip}}:{{puerto}}/api/books. En la petición anterior se creaba un libro en el que el atributo ISBN no estaba presente en el body; en esta petición el atributo está presente pero tiene como valor una cadena vacía, que no está permitida por las reglas definidas en la lógica.

El test es semejante al de la petición anterior.

Esta es la respuesta cuando se ejecuta el request. Se observa que el API ha respondido con un error (apierror) que tiene tres atributos: el status de la petición (PRECONDITION_FAILED), la marca de tiempo cuando ocurrió el error y el mensaje de explicación.

A nivel de test se observa que ambos test pasan correctamente porque el libro no se ha creado y el mensaje de error es el esperado.

Crear un libro

En este request se crea un libro. Este es un request de tipo POST. La url de la petición eshttp://{{ip}}:{{puerto}}/api/books. Acá todos los atributos han sido seteados correctamente.

A nivel de pruebas se almacena el id del libro creado en la variable new_id_b y se espera que el código de respuesta sea 201.

Cuando se ejecuta el request se obtiene el nuevo libro creado (identificado con el número 1) al que se asignado la editorial creada en el primer request.

La prueba también se ha ejecutado de forma correcta.

Crear un libro 2

En este request se crea otro libro con nuevos valores. Este es un request de tipo POST. La url de la petición eshttp://{{ip}}:{{puerto}}/api/books. Acá todos los atributos han sido seteados correctamente.

A nivel de pruebas se almacena el id del libro creado en la variable new_id_b2 y se espera que el código de respuesta sea 201.

Cuando se ejecuta el request se obtiene el nuevo libro creado (identificado con el número 2) al que se asignado la editorial creada en el primer request.

La prueba también se ha ejecutado de forma correcta.

Crear un libro con el mismo ISBN

En este request se crea un libro con un ISBN de un libro que ya ha sido creado previamente. Este es un request de tipo POST. La url de la petición eshttp://{{ip}}:{{puerto}}/api/books.

A nivel de pruebas se espera que el API retorne el código de respuesta 412 junto con el mensaje de error que indica que el ISBN ya existe.

Cuando se ejecuta el request se obtiene un error.

La prueba también se ha ejecutado de forma correcta.

Crear un libro con editorial que no existe

En este request se crea un libro con una editorial que no existe (en este caso la editorial con el id 0). Este es un request de tipo POST. La url de la petición es http://{{ip}}:{{puerto}}/api/books.

A nivel de pruebas se espera que el API retorne el código de respuesta 412 junto con el mensaje de error que indica que la editorial no es válida.

Cuando se ejecuta el request se obtiene un error.

La prueba se ha ejecutado de forma correcta.

@GetMapping
        @ResponseStatus(code = HttpStatus.OK)
        public List<BookDetailDTO> findAll() {
                List<BookEntity> books = bookService.getBooks();
                return modelMapper.map(books, new TypeToken<List<BookDetailDTO>>() {
                }.getType());
        }

Como podemos observar, el método necesita las anotaciones @GetMapping, que indica que se trata de un método que se resuelve a partir de una petición HTTP de tipo GET. Esto significa que mediante una petición GET a la URL http://localhost:8080/api/books obtendremos la lista de libros. También tenemos la anotación @ResponseStatus, la cual incluirá en la respuesta el código 200. En el cuerpo de la implementación se llama al método getBooks del servicio que retorna una lista de BookEntity. Esta lista se pasa al model mapper y se convierte en una lista de BookDetailDTO que es la respuesta de este método.

Prueba para obtener todos los libros

En este request se obtiene el listado de todos los libros. Este es un request de tipo GET. La url de la petición eshttp://{{ip}}:{{puerto}}/api/books. Esta es una petición que no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 200. En la variable data se almacena la respuesta (un JSON) y se espera que el número de elementos de ese JSON sea mayor que cero. .

Cuando se ejecuta el request se obtiene el listado de los libros creados previamente.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200 y el número de elementos retornado es mayor que cero.

@GetMapping(value = "/{id}")
        @ResponseStatus(code = HttpStatus.OK)
        public BookDetailDTO findOne(@PathVariable("id") Long id) throws EntityNotFoundException {
                BookEntity bookEntity = bookService.getBook(id);
                return modelMapper.map(bookEntity, BookDetailDTO.class);
        }

En este método se incluye la anotación @GetMapping con el atributo value = "/id". Es decir, este método resuelve peticiones get de la forma:

GET http://localhost:8080/api/books/100

Este valor del id (100 en el caso del ejemplo) en la ruta de la petición se captura con la anotación @PathVariable("id"). Este id representa el identificador del libro que queremos consultar.

En la implementación se llama al método de la lógica que obtiene un libro por su id. La lógica retorna un objeto BookEntity que es pasado al model mapper para ser convertido en un objeto de tipo BookDetailDTO. Nótese que en caso de que el libro con el id proporcionado no exista, se lanzará una excepción.

Prueba para obtener un libro

En este request se obtiene un libro por su id. Este es un request de tipo GET. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}. Esta es una petición que no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 200.

Cuando se ejecuta el request se obtiene el libro con el id 1 que es el valor almacenado en la variable new_id_b.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200.

Prueba para obtener un libro que no existe

En este request se obtiene un libro por su id. Este es un request de tipo GET. La url de la petición es http://{{ip}}:{{puerto}}/api/books/0. Esta es una petición que no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 (not found) que indica que el recurso (el libro) identificado con el número 0 no existe. También se espera un mensaje de error que indica que el libro con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 junto con el mensaje de error esperado.

@PutMapping(value = "/{id}")
        @ResponseStatus(code = HttpStatus.OK)
        public BookDTO update(@PathVariable("id") Long id, @RequestBody BookDTO bookDTO)
                        throws EntityNotFoundException, IllegalOperationException {
                BookEntity bookEntity = bookService.updateBook(id, modelMapper.map(bookDTO, BookEntity.class));
                return modelMapper.map(bookEntity, BookDTO.class);
        }

En este método se usa la anotación @PutMapping, para mapear a este método cuando se invoque una petición de tipo PUT a la ruta especificada.

Este método resuelve peticiones PUT de la forma:

PUT http://localhost:8080/api/books/100

Al tratarse de una función de actualización, se necesita conocer el id del objeto que queremos modificar, además de los atributos que se deben reemplazar. Esto lo hacemos a través de las anotaciones @PathVariable("id") Long id, @RequestBody BookDTO bookDTO en los parámetros del método. En la implementación se llama al servicio de la lógica encargado de la actualización y retorna el objeto actualizado. Puede ocurrir que el id especificado no se encuentre por lo que el método puede lanzar la excepción EntityNotFoundException.

Prueba para editar un libro que no existe

En este request se edita un libro por su id. Este es un request de tipo PUT. La url de la petición es http://{{ip}}:{{puerto}}/api/books/0. El body de la petición contiene los nuevos datos para el libro.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 (not found) que indica que el recurso (el libro) identificado con el número 0 no existe. También se espera un mensaje de error que indica que el libro con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 junto con el mensaje de error esperado.

Prueba para editar un libro con un ISBN inválido

En este request se edita un libro por su id. Este es un request de tipo PUT. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}. El body de la petición contiene los nuevos datos para el libro.

A nivel de pruebas se espera que el API retorne el código de respuesta 412. También se espera un mensaje de error que indica que el ISBN no es válido.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 junto con el mensaje de error esperado.

Prueba para editar un libro

En este request se edita un libro por su id. Este es un request de tipo PUT. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}. El body de la petición contiene los nuevos datos para el libro.

A nivel de pruebas se espera que el API retorne el código de respuesta 200.

Cuando se ejecuta el obtiene el libro identificado con el número 1 con los nuevos datos.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200.

@DeleteMapping(value = "/{id}")
        @ResponseStatus(code = HttpStatus.NO_CONTENT)
        public void delete(@PathVariable("id") Long id) throws EntityNotFoundException, IllegalOperationException {
                bookService.deleteBook(id);
        }

En este método se usa la anotación @DeleteMapping que referencia al método cuando se hace una petición DELETE a través del protocolo HTTP.

Este método resuelve peticiones DELETE de la forma:

DELETE http://localhost:8080/api/books/100

El atributo HttpStatus.NO_CONTENT indica que la respuesta de este método es vacía. En la lógica, la eliminación de un libro es un método que contiene una regla de negocio (no se puede borrar un libro si tiene un autor asociado), por lo que esta puede arrojar la excepción IllegalOperationException.

Prueba para borrar un libro

En este request se borra un libro por su id. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}. Este request no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 204, es decir que la respuesta incluye un contenido

Cuando se ejecuta el request el body está vacío.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 204.

Prueba para borrar un libro 2

En este request se borra un libro por su id. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b2}}. Este request no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 204, es decir que la respuesta incluye un contenido

Cuando se ejecuta el request el body está vacío.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 204.

Prueba para borrar un libro que no existe

En este request se borra un libro por su id. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/0. Este request no tiene body.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 junto con el mensaje de error que indica que el libro con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene un mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje esperado de error.

Como hemos visto a lo largo del proyecto, podemos encontrar distintos tipos de relaciones en nuestras entidades. Para el caso de los recursos Author y Book se han creado dos controladores para manejar las asociaciones entre las clases: AuthorBookController y BookAuthorController. En este tutorial vamos a centrarnos en la clase BookAuthorController.

Antes de empezar con los distintos métodos de la clase vale aclarar que esta está denotada con la anotación @RequestMapping("book") al igual que el controlador de BookController. Esto sucede debido a que estamos agregando más funciones específicas a esta misma ruta, en este caso para manejar la relación con autores.

@PostMapping(value = "/{bookId}/authors/{authorId}")
        @ResponseStatus(code = HttpStatus.OK)
        public AuthorDetailDTO addAuthor(@PathVariable("authorId") Long authorId, @PathVariable("bookId") Long bookId)
                        throws EntityNotFoundException {
                AuthorEntity authorEntity = bookAuthorService.addAuthor(bookId, authorId);
                return modelMapper.map(authorEntity, AuthorDetailDTO.class);
        }

En este método asocia un autor con un libro. El método resuelve peticiones GET de la forma:

POST http://localhost:8080/api/books/100/authors/100

Esto significa que al libro con el id 100 se le asocia el autor con el id 100. Ambos atributos se reciben en el método en las variables bookId y authorId respectivamente. Estos valores se pasan al método del servicio el cual retornará el autor asociado. En caso de que el autor o el libro no existan se lanzará una excepción; también ocurre una excepción si el autor no está asociado al libro.

Prueba para asociar un autor a un libro

Para poder asociar un autor a un libro se requieren varios pasos previos: a) crear una editorial; b) crear un autor 1; c) crear un autor 2; y d) crear un libro.

En la colección BookAutors Test puede observar estos 4 requests.

Luego de esto ejecutaremos el request Agregar un autor a un libro. En este request se agrega un autor un un libro. Este es un request de tipo POST. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/{{new_id_a}}. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 200.

Cuando se ejecuta el request se obtiene el autor con el nuevo libro asignado.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200.

Prueba para agregar un autor que no existe a un libro

En este request se agrega un autor que no existe a un un libro. Este es un request de tipo POST. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/0. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 y un mensaje de error indicando que el autor con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje de error esperado.

@GetMapping(value = "/{bookId}/authors/{authorId}")
        @ResponseStatus(code = HttpStatus.OK)
        public AuthorDetailDTO getAuthor(@PathVariable("authorId") Long authorId, @PathVariable("bookId") Long bookId)
                        throws EntityNotFoundException, IllegalOperationException {
                AuthorEntity authorEntity = bookAuthorService.getAuthor(bookId, authorId);
                return modelMapper.map(authorEntity, AuthorDetailDTO.class);
        }

En este método se obtiene el autor de un libro. El método resuelve peticiones GET de la forma:

GET http://localhost:8080/api/books/100/authors/100

Esto significa que al libro con el id 100 se busca el autor con el id 100. Ambos atributos se reciben en el método en las variables bookId y authorId respectivamente. Estos valores se pasan al método del servicio el cual retornará el autor asociado. En caso de que el autor o el libro no existan se lanzará una excepción; también ocurre una excepción si el autor no está asociado al libro.

Prueba para obtener el autor de un libro

En este request se obtiene el autor de un libro. Este es un request de tipo GET. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/{{new_id_a}}. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 200.

Cuando se ejecuta el request se obtiene el autor del libro.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200.

Prueba para obtener un autor que no existe de un libro

En este request se obtiene un autor que no existe de un libro. Este es un request de tipo GET. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/0. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 y un mensaje que indica que el autor con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje de error esperado.

@PutMapping(value = "/{bookId}/authors")
        @ResponseStatus(code = HttpStatus.OK)
        public List<AuthorDetailDTO> addAuthors(@PathVariable("bookId") Long bookId, @RequestBody List<AuthorDTO> authors)
                        throws EntityNotFoundException {
                List<AuthorEntity> entities = modelMapper.map(authors, new TypeToken<List<AuthorEntity>>() {
                }.getType());
                List<AuthorEntity> authorsList = bookAuthorService.replaceAuthors(bookId, entities);
                return modelMapper.map(authorsList, new TypeToken<List<AuthorDetailDTO>>() {
                }.getType());
        }

En este método se reemplaza el listado de autores de un libro. El método resuelve peticiones PUT de la forma:

PUT http://localhost:8080/api/books/100/authors/

Esto significa que obtendrá todos los autores de un libro en particular (el libro con el id 100 para el caso del ejemplo). Para empezar la lista de autores que se recibe como parte del body de la petición se convierte en una lista de AuthorEntity. Esa lista se pasa al método del servicio junto con el id del libro al cual se quiere reemplazar los autores. El método retorna la nueva lista de autores la cual se convierte en una lista de de AuthorDetailDTO.

Prueba para actualizar los autores de un libro

En este request se actualiza la lista de autores de un libro. Este es un request de tipo PUT. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors. El body de la petición contiene el listado de nuevos autores.

A nivel de pruebas se espera que el API retorne el código de respuesta 200.

Cuando se ejecuta el request se obtienen los nuevos autores con el libro asociado.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200.

Prueba para asociar autores a un libro que no existe

En este request se asocian nuevos autores a un libro que no existe. Este es un request de tipo PUT. La url de la petición es http://{{ip}}:{{puerto}}/api/books/0/authors. El body de la petición contiene el listado de nuevos autores.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 y un mensaje que indica que el libro con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene el mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje de error esperado.

@GetMapping(value = "/{bookId}/authors")
        @ResponseStatus(code = HttpStatus.OK)
        public List<AuthorDetailDTO> getAuthors(@PathVariable("bookId") Long bookId) throws EntityNotFoundException {
                List<AuthorEntity> authorEntity = bookAuthorService.getAuthors(bookId);
                return modelMapper.map(authorEntity, new TypeToken<List<AuthorDetailDTO>>() {
                }.getType());
        }

En este método se obtiene la lista de autores de un libro. El método resuelve peticiones de la forma:

GET http://localhost:8080/api/books/100/authors/

Esto quiere decir que se obtienen los autores de un libro en específico (el libro con el id 100 para el caso del ejemplo). El método obtiene una lista de AuthorEntity que se mapea a una lista de AuthorDetailDTO para ser retornados al cliente.

Prueba para obtener los autores de un libro

En este request se obtienen todos los autores de un libro. Este es un request de tipo GET. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 202 y que el listado de autores contenga al menos un resultado.

Cuando se ejecuta el request se obtiene el listado de autores del libro.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 200 y el tamaño del listado de autores es mayor o igual a uno.

@DeleteMapping(value = "/{bookId}/authors/{authorId}")
        @ResponseStatus(code = HttpStatus.NO_CONTENT)
        public void removeAuthor(@PathVariable("authorId") Long authorId, @PathVariable("bookId") Long bookId)
                        throws EntityNotFoundException {
                bookAuthorService.removeAuthor(bookId, authorId);
        }

En este método se borra un autor de un libro. Este método resuelve peticiones de tipo DELETE de la forma:

DELETE http://localhost:8080/api/books/100/authors/100

El método tiene un tipo de retorno void. Lanzará una excepción si el autor con el id proporcionado no existe.

Prueba para borrar el autor de un libro

En este request se borra el autore de un libro. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/{{new_id_a}}.El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 204.

Cuando se ejecuta el request se obtiene una respuesta vacía.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 204.

Prueba para borrar un autor a un libro que no existe

En este request se borra el autor de un libro que no existe. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/0/authors/{{new_id_a}}. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 y un mensaje en el que se indica que el libro con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene un mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje de error esperado.

Prueba para borrar un autor que no existe a un libro

En este request se borra un autor que no existe de un libro. Este es un request de tipo DELETE. La url de la petición es http://{{ip}}:{{puerto}}/api/books/{{new_id_b}}/authors/0. El body de la petición está vacío.

A nivel de pruebas se espera que el API retorne el código de respuesta 404 y un mensaje en el que se indica que el autor con el id proporcionado no existe.

Cuando se ejecuta el request se obtiene un mensaje de error.

La prueba se ha ejecutado de forma correcta, es decir, se ha recibido como respuesta el código 404 y el mensaje de error esperado.