Skip to content

Latest commit

 

History

History
261 lines (162 loc) · 11.3 KB

DEV_WALKTHROUGH.md

File metadata and controls

261 lines (162 loc) · 11.3 KB

Development Walkthrough

1st STEP

🎉 Creating a new project

  1. Create a project from Spring Initializr

    • Java 17
    • Maven 3.1.2
    • Dependecies
      • Spring Boot DevTools
      • Spring Data JPA
      • Lombok
      • Flyway Migration
      • MySQL Driver
  2. Add extra dependencies

    milhasapi/pom.xml

    Lines 35 to 46 in bce5a45

    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
    <version>9.21.0</version>
    </dependency>
    <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.1.0</version>
    </dependency>

  3. Initial configuration

    spring.datasource.url=jdbc:mysql://mysqldev:3306/milhas_api
    spring.datasource.username=root
    spring.datasource.password=123

     ℹ️ `mysqldev` is the mysql container name that is running in the same container network as my java development container
    
  4. Create Database

    CREATE DATABASE milhas_api;
  5. Run it 🏁

    Now you can run the project and access http://javahost:8080/swagger-ui/index.html

    Swagger-UI Page

🔧 First Swagger configuration

@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Milhas API")
.description("API Rest for Milhas application")
.version("0.0.1")
.contact(new Contact()
.name("ecureuill")
.email("logikasciuro@gmail.com")));
}
}

Updated Swagger-UI

👩‍💻 Testimonals

  1. Create table testimonials through flyway migration file

    create table testimonials(
    id bigint not null auto_increment,
    name varchar(100) not null,
    testimonial varchar(100) not null,
    photo varchar(100),
    primary key(id)
    );

  2. Create entity

    @Table(name = "testimonials")
    @Entity(name = "Testimonial")
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode(of = "id")
    public class Testimonial {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String testimonial;
    private String photo;
    public Testimonial(TestimonialCreateRecord record) {
    this.name = record.name();
    this.testimonial = record.testimonial();
    this.photo = record.photo();
    }
    }

  3. Create repository

    public interface TestimonialRepository extends JpaRepository<Testimonial, Long>{
    }

👩‍💻 Post request

  1. Create TestimonialDetailRecord and TestimonialCreateRecord

    public record TestimonialDetailRecord(
    String name,
    String photo,
    String testimonial
    ) {
    public TestimonialDetailRecord(Testimonial doctor) {
    this(doctor.getName(), doctor.getPhoto(), doctor.getTestimonial());
    }
    }

    public record TestimonialCreateRecord(
    @NotBlank
    String name,
    String photo,
    @NotBlank
    String testimonial
    ) {
    }

  2. Create depoimentos endpoint and POST request

    @RestController
    @RequestMapping("/depoimentos")
    public class TestimonialController {
    @Autowired
    private TestimonialRepository repository;
    @PostMapping
    @Transactional
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<TestimonialDetailRecord> save(@RequestBody @Valid TestimonialCreateRecord record, UriComponentsBuilder uriBuilder){
    var doctor = repository.save(new Testimonial(record));
    var uri = uriBuilder.path("/doctors/{id}").buildAndExpand(doctor.getId()).toUri();
    var dto = new TestimonialDetailRecord(doctor);
    return ResponseEntity.created(uri).body(dto);
    }
    }

  3. Run it 🏁

    You can check in swagger, the newly created endpoint

    Swagger-UI

👩‍💻 Get, PUT and DELETE requests

  1. Create TestimonialUpdateRecord

    public record TestimonialUpdateRecord(
    @NotBlank
    String name,
    @NotBlank
    String testimonial,
    String photo
    ) {
    }

  2. Update TestimonialController

    @GetMapping
    public ResponseEntity<List<TestimonialDetailRecord>> getAll(){
    return ResponseEntity.ok().body(repository.findAll().stream().map(TestimonialDetailRecord::new).collect(Collectors.toList()));
    }
    @PutMapping(value="/{id}")
    @Transactional
    public ResponseEntity<TestimonialDetailRecord> update(@PathVariable Long id, @RequestBody TestimonialUpdateRecord record) {
    var data = repository.findById(id);
    if (data.isEmpty()) {
    throw new EntityNotFoundException();
    }
    var testimonial = data.get();
    testimonial.update(record);
    return ResponseEntity.ok().body(new TestimonialDetailRecord(testimonial));
    }
    @DeleteMapping(value="/{id}")
    @Transactional
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ResponseEntity<Void> delete(@PathVariable Long id) {
    var data = repository.findById(id);
    if (data.isEmpty()) {
    throw new EntityNotFoundException();
    }
    repository.deleteById(id);
    return ResponseEntity.noContent().build();
    }

  3. Run it 🏁

    ℹ️ You can check in swagger, the newly created requests verbs

👩‍💻 Create depoimentos-home endpoint

  1. Add findThreeTestimonials query to TestimonialRepository

    @Query("""
    select t from Testimonial t
    order by rand()
    limit 3
    """)
    List<Testimonial> findThreeTestimonials();

  2. Create RandomTestimonialController

    @RestController
    @RequestMapping("/depoimentos-home")
    public class RandomTestimonialController {
    @Autowired
    private TestimonialRepository repository;
    @GetMapping
    public ResponseEntity<List<TestimonialDetailRecord>> get() {
    var testimonials = repository.findThreeTestimonials();
    return ResponseEntity.ok().body(testimonials.stream().map(TestimonialDetailRecord::new).collect(Collectors.toList()));
    }
    }

🔍 Unit Testing Controllers

  1. Add datafaker depedency

    milhasapi/pom.xml

    Lines 21 to 25 in 5b8313f

    <dependency>
    <groupId>net.datafaker</groupId>
    <artifactId>datafaker</artifactId>
    <version>2.0.1</version>
    </dependency>

  2. Create fake data

    public class GenerateData {
    private static Faker faker = new Faker();
    public static Testimonial randomTestimonial() {
    return new Testimonial(
    faker.random().nextLong(),
    faker.funnyName().name(),
    faker.lorem().sentence(),
    faker.internet().url()
    );
    }
    public static List<Testimonial> randomTestimonials(int count) {
    return IntStream.range(0, count)
    .mapToObj(i -> randomTestimonial())
    .collect(Collectors.toList());
    }
    }

  3. Create TestimonialControllerUnitTest

    @SpringBootTest
    @AutoConfigureMockMvc
    @AutoConfigureJsonTesters
    public class TestimonialControllerUnitTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private JacksonTester<TestimonialDetailRecord> testimonialDetailRecord;
    @Autowired
    private JacksonTester<List<TestimonialDetailRecord>> testimonialsDetailRecord;
    @Autowired
    private JacksonTester<TestimonialCreateRecord> testimonialCreateRecord;
    @Autowired
    JacksonTester<TestimonialUpdateRecord> testimonialUpdateRecord;
    @MockBean
    private TestimonialRepository repository;
    @Test
    @DisplayName("Should return status OK")
    void testGetAllStatusCode() throws Exception {
    var data = Collections.<Testimonial>emptyList();
    Mockito.when(repository.findAll()).thenReturn(data);
    var response = mockMvc
    .perform(get("/depoimentos"))
    .andReturn().getResponse();
    var record = Collections.<TestimonialDetailRecord>emptyList();
    var json = testimonialsDetailRecord.write(record).getJson();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
    assertThat(response.getContentAsString()).isEqualTo(json);
    }
    @Test
    @DisplayName("Should return list of TestimonialDetailRecord")
    void testGetAllContent() throws Exception {
    var data = GenerateData.randomTestimonials(4);
    Mockito.when(repository.findAll()).thenReturn(data);
    var response = mockMvc
    .perform(get("/depoimentos"))
    .andReturn().getResponse();
    var record = data.stream().map(TestimonialDetailRecord::new).collect(Collectors.toList());
    var json = testimonialsDetailRecord.write(record).getJson();
    assertThat(response.getContentAsString()).isEqualTo(json);
    }
    @Test
    @DisplayName("Should return empty list of TestimonialDetailRecord")
    void testGetAllStatusEmptyContent() throws Exception {
    var data = Collections.<Testimonial>emptyList();
    Mockito.when(repository.findAll()).thenReturn(data);
    var response = mockMvc
    .perform(get("/depoimentos"))
    .andReturn().getResponse();
    var record = Collections.<TestimonialDetailRecord>emptyList();
    var json = testimonialsDetailRecord.write(record).getJson();
    assertThat(response.getContentAsString()).isEqualTo(json);
    }
    @Test
    @DisplayName("Should return status CREATED when request body is valid")
    void testSaveStatusCodeCreated() throws Exception {
    var data = GenerateData.randomTestimonial();
    var record = new TestimonialCreateRecord(data.getName(), data.getPhoto(), data.getTestimonial());
    var json = testimonialCreateRecord.write(record).getJson();
    Mockito.when(repository.save(any())).thenReturn(data);
    var response = mockMvc
    .perform(post("/depoimentos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andReturn().getResponse();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
    }
    @Test
    @DisplayName("Should return status BAD REQUEST when request body is invalid")
    void testSaveStatusCodeBadRequest() throws Exception {
    var record = new TestimonialCreateRecord(null, null, null);
    var json = testimonialCreateRecord.write(record).getJson();
    var response = mockMvc
    .perform(post("/depoimentos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andReturn().getResponse();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
    }
    @Test
    @DisplayName("Should return created TestimonialDetailRecord when request body is valid")
    void testSaveContent() throws Exception {
    var data = GenerateData.randomTestimonial();
    var requestJson = testimonialCreateRecord.write(new TestimonialCreateRecord(data.getName(), data.getPhoto(), data.getTestimonial())).getJson();
    var responseJson = testimonialDetailRecord.write(new TestimonialDetailRecord(data)).getJson();
    Mockito.when(repository.save(any())).thenReturn(data);
    var response = mockMvc
    .perform(post("/depoimentos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(requestJson))
    .andReturn().getResponse();
    assertThat(response.getContentAsString()).isEqualTo(responseJson);
    }
    @Test
    @DisplayName("Should return status BAD REQUEST when request body is invalid")
    void testUpdateStatusCodeBadRequest() throws Exception {
    var record = new TestimonialUpdateRecord(null, null, null);
    var json = testimonialUpdateRecord.write(record).getJson();
    var response = mockMvc
    .perform(put("/depoimentos/{id}", 1)
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andReturn().getResponse();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
    }
    @Test
    @DisplayName("Should return status OK when request body is valid")
    void testUpdateStatusCodeOK() throws Exception {
    var data = GenerateData.randomTestimonial();
    var updatedData = GenerateData.randomTestimonial();
    var record = new TestimonialUpdateRecord(updatedData.getName(), updatedData.getTestimonial(), updatedData.getPhoto());
    var json = testimonialUpdateRecord.write(record).getJson();
    Mockito.when(repository.getReferenceById(any())).thenReturn(data);
    var response = mockMvc
    .perform(put("/depoimentos/{id}", data.getId())
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andReturn().getResponse();
    assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
    }
    @Test
    @DisplayName("Should return updated TestimonialDetailRecord when request body is valid")
    void testUpdateContent() throws Exception {
    var data = GenerateData.randomTestimonial();
    var requestJson = testimonialUpdateRecord.write(new TestimonialUpdateRecord(data.getName(), "altered testimonial", data.getPhoto())).getJson();
    var responseJson = testimonialDetailRecord.write(new TestimonialDetailRecord(data.getName(),data.getPhoto(), "altered testimonial")).getJson();
    Mockito.when(repository.getReferenceById(any())).thenReturn(data);
    var response = mockMvc
    .perform(put("/depoimentos/{id}", data.getId())
    .contentType(MediaType.APPLICATION_JSON)
    .content(requestJson))
    .andReturn().getResponse();
    assertThat(response.getContentAsString()).isEqualTo(responseJson);
    }
    @Test
    @DisplayName("Should throw EntityNotFound and status NOTFOUND when path variable not exist")
    void testUpdateStatusCodeNotFound() throws Exception {
    var data = GenerateData.randomTestimonial();
    var requestJson = testimonialUpdateRecord.write(new TestimonialUpdateRecord(data.getName(), "altered testimonial", data.getPhoto())).getJson();
    Mockito.when(repository.getReferenceById(any())).thenThrow(new EntityNotFoundException("Unable to find ecureuill.milhasapi.domain.testimonial.Testimonial with id 99999"));
    mockMvc
    .perform(put("/depoimentos/{id}", data.getId())
    .contentType(MediaType.APPLICATION_JSON)
    .content(requestJson))
    .andExpect(status().isNotFound())
    .andExpect(result -> assertTrue(result.getResolvedException() instanceof EntityNotFoundException));
    }
    @Test
    @DisplayName("Should return status NO CONTENT")
    void testDeleteStatucCodeNoContent() throws Exception {
    mockMvc
    .perform(delete("/depoimentos/{id}", 4))
    .andExpect(status().isNoContent());
    Mockito.verify(repository).deleteById(any());
    }
    }

  4. Run tests 🧪

    Tests output

  5. Fix code 🐛

    @PutMapping(value="/{id}")
    @Transactional
    public ResponseEntity<TestimonialDetailRecord> update(@PathVariable Long id, @RequestBody @Valid TestimonialUpdateRecord record) {
    var testimonial = repository.getReferenceById(id);
    testimonial.update(record);
    return ResponseEntity.ok().body(new TestimonialDetailRecord(testimonial));
    }
    @DeleteMapping(value="/{id}")
    @Transactional
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ResponseEntity<Void> delete(@PathVariable Long id) {
    repository.deleteById(id);
    return ResponseEntity.noContent().build();
    }

     ⚠️ Check full code alteration on [commit](https://github.com/ecureuill/milhasapi/commit/5b8313f2eb50efcbd5606cd6d55952ecb38292b8)
    
  6. Run test again 🧪

    The only failing test should be testUpdateStatusCodeNotFound due to all exception is being handler by Spring that returns status code 500.

    Tests output

🔧 CORS

public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}


2nd STEP

👩‍💻 Destinations

Pretty much the same as Testimonials

  1. Create table destinations through flyway migration file

    create table destinations(
    id bigint not null auto_increment,
    name varchar(100) not null,
    photo varchar(100) not null,
    price decimal not null,
    primary key(id)
    );

  2. Create entity

    @Table(name = "destinations")
    @Entity(name = "Destination")
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @EqualsAndHashCode(of = "id")
    public class Destination {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String photo;
    private BigDecimal Price;
    }

  3. Create repository

    ❇️

    Spring Data JPA derived query

    List<Destination> findAllByNameStartingWith(String name);

    public interface DestinationRepository extends JpaRepository<Destination, Long>{
    List<Destination> findAllByNameStartingWith(String name);
    }

  4. Create records

    public record DestinationDetailRecord(
    Long id,
    String name,
    String photo,
    BigDecimal price
    ) {
    public DestinationDetailRecord(Destination destination) {
    this(
    destination.getId(),
    destination.getName(),
    destination.getPhoto(),
    destination.getPrice()
    );
    }
    }

    public record DestinationCreateRecord(
    @NotBlank
    String name,
    @NotBlank
    String photo,
    @DecimalMin(value = "0.0", inclusive = false)
    @Digits(integer = 5, fraction=2)
    BigDecimal price
    ) {
    }

    public record DestinationUpdateRecord(
    @NotBlank
    String name,
    @NotBlank
    String photo,
    @DecimalMin(value = "0.0", inclusive = false)
    @Digits(integer = 5, fraction=2)
    BigDecimal price
    ) {
    }

  5. Create controller

    public Destination(@Valid DestinationCreateRecord record) {
    this.name = record.name();
    this.photo = record.photo();
    this.Price = record.price();
    }
    public void update(@Valid DestinationUpdateRecord record) {
    this.name = record.name();
    this.photo = record.photo();
    this.Price = record.price();
    }

    @RestController
    @RequestMapping("/destinos")
    public class DestinationController {
    @Autowired
    private DestinationRepository repository;
    @PostMapping
    @Transactional
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<DestinationDetailRecord> create(@Valid @RequestBody DestinationCreateRecord record, UriComponentsBuilder uriBuilder) {
    var destination = repository.save(new Destination(record));
    var uri = uriBuilder.path("destinos/{id}").buildAndExpand(destination.getId()).toUri();
    return ResponseEntity.created(uri).body(new DestinationDetailRecord(destination));
    }
    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<List<DestinationDetailRecord>> getAll(@RequestParam(name="name", required = false) String name) {
    if(name != null){
    var result = repository.findAllByNameStartingWith(name);
    if (result.isEmpty()) {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Nenhum destino foi encontrado", null);
    }
    return ResponseEntity.ok().body(result.stream().map(DestinationDetailRecord::new).collect(Collectors.toList()));
    }
    return ResponseEntity.ok().body(repository.findAll().stream().map(DestinationDetailRecord::new).collect(Collectors.toList()));
    }
    @GetMapping(value = "{id}")
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<DestinationDetailRecord> getOne(@PathVariable Long id) {
    var data = repository.getReferenceById(id);
    return ResponseEntity.ok().body(new DestinationDetailRecord(data));
    }
    @PutMapping(value = "/{id}")
    @Transactional
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<DestinationDetailRecord> update(@PathVariable Long id, @Valid @RequestBody DestinationUpdateRecord record) {
    var destination = repository.getReferenceById(id);
    destination.update(record);
    return ResponseEntity.ok().body(new DestinationDetailRecord(destination));
    }
    @DeleteMapping(value = "{id}")
    @Transactional
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ResponseEntity<Void> delete(@PathVariable Long id) {
    repository.deleteById(id);
    return ResponseEntity.noContent().build();
    }
    }

    ❇️

    Use of optional parameter to Get request

    public ResponseEntity<List<DestinationDetailRecord>> getAll(@RequestParam(name="name", required = false) String name)

    Communicate the failure of the HTTP request

    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Nenhum destino foi encontrado", null);

🔍 Unit Testing Controllers

  1. Create fake data

    public static Destination randomDestination(){
    return new Destination(
    faker.random().nextLong(),
    faker.address().cityName(),
    faker.internet().url(),
    new BigDecimal(200)
    );
    }
    public static List<Destination> randomDestinations(int count) {
    return IntStream.range(0, count)
    .mapToObj(i -> randomDestination())
    .collect(Collectors.toList());
    }

  2. Create DestinationControllerUnitTest

    @SpringBootTest
    @AutoConfigureMockMvc
    @AutoConfigureJsonTesters
    public class DestinationControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    JacksonTester<DestinationCreateRecord> destinationCreateRecord;
    @Autowired
    JacksonTester<DestinationDetailRecord> destinationDetailRecord;
    @Autowired
    JacksonTester<DestinationUpdateRecord> destinationUpdateRecord;
    @MockBean
    private DestinationRepository repository;
    @Test
    @DisplayName("Should return DestinationDetailed object when body is valid")
    void testSaveStatusCodeCreated() throws Exception {
    var data = GenerateData.randomDestination();
    var record = new DestinationCreateRecord(data.getName(), data.getPhoto(), data.getPrice());
    var json = destinationCreateRecord.write(record).getJson();
    var responseJson = destinationDetailRecord.write( new DestinationDetailRecord(data.getId(), data.getName(), data.getPhoto(), data.getPrice())).getJson();
    Mockito.when(repository.save(any())).thenReturn(data);
    mockMvc.perform(
    post("/destinos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andExpect(MockMvcResultMatchers.content().json(responseJson));
    }
    @Test
    @DisplayName("Should return status CREATED when body is valid")
    void testSaveContent() throws Exception {
    var data = GenerateData.randomDestination();
    var record = new DestinationCreateRecord(data.getName(), data.getPhoto(), data.getPrice());
    var json = destinationCreateRecord.write(record).getJson();
    Mockito.when(repository.save(any())).thenReturn(data);
    mockMvc.perform(
    post("/destinos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andExpect(status().isCreated());
    }
    @Test
    @DisplayName("Should return status BADREQUEST when body is invalid")
    void testSaveStatusBadRequest() throws Exception {
    var record = new DestinationCreateRecord(null, null, null);
    var json = destinationCreateRecord.write(record).getJson();
    mockMvc.perform(
    post("/destinos")
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andExpect(status().isBadRequest());
    }
    @Test
    @DisplayName("Should return status OK when list is empty")
    void testGetAllStatusOKEmptyList() throws Exception {
    var data = Collections.<Destination>emptyList();
    Mockito.when(repository.findAll()).thenReturn(data);
    mockMvc.perform(
    get("/destinos"))
    .andExpect(status().isOk());
    }
    @Test
    @DisplayName("Should return status OK")
    void testGetAllStatusOK() throws Exception {
    var data = GenerateData.randomDestinations(3);
    Mockito.when(repository.findAll()).thenReturn(data);
    mockMvc.perform(
    get("/destinos"))
    .andExpect(status().isOk());
    }
    @Test
    @DisplayName("Should return status OK when filter by name")
    void testGetAllStatusOKFilterByName() throws Exception {
    var data = GenerateData.randomDestinations(3);
    var nameFilter = "abc";
    Mockito.when(repository.findAllByNameStartingWith(nameFilter)).thenReturn(data);
    mockMvc.perform(
    get("/destinos")
    .param("name",nameFilter))
    .andExpect(status().isOk());
    }
    @Test
    @DisplayName("Should return status NotFound when filter by not existent name")
    void testGetAllStatusNotFoundFilterByName() throws Exception {
    var data = Collections.<Destination>emptyList();
    var nameFilter = "abc";
    Mockito.when(repository.findAllByNameStartingWith(nameFilter)).thenReturn(data);
    mockMvc.perform(
    get("/destinos")
    .param("name",nameFilter))
    .andExpect(status().isNotFound());
    }
    @Test
    @DisplayName("Should return status OK when get one")
    void testGetOneStatusOK() throws Exception {
    var data = GenerateData.randomDestination();
    Mockito.when(repository.getReferenceById(data.getId())).thenReturn(data);
    mockMvc.perform(
    get("/destinos/{id}", data.getId()))
    .andExpect(status().isOk());
    }
    @Test
    @DisplayName("Should return status NOT FOUND when get one with not existent id")
    void testGetOneStatusNotFound() throws Exception {
    var data = GenerateData.randomDestination();
    Mockito.when(repository.getReferenceById(data.getId())).thenThrow(EntityNotFoundException.class);
    mockMvc.perform(
    get("/destinos/{id}", data.getId()))
    .andExpect(status().isNotFound());
    }
    @Test
    void testUpdateStatusOK()throws Exception {
    var data = GenerateData.randomDestination();
    var record = new DestinationUpdateRecord(data.getName(), data.getPhoto(), data.getPrice());
    var json = destinationUpdateRecord.write(record).getJson();
    Mockito.when(repository.getReferenceById(data.getId())).thenReturn(data);
    mockMvc
    .perform(put("/destinos/{id}", data.getId())
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andExpect(status().isOk());
    }
    @Test
    void testUpdateStatusNotFound()throws Exception {
    var data = GenerateData.randomDestination();
    var record = new DestinationUpdateRecord(data.getName(), data.getPhoto(), data.getPrice());
    var json = destinationUpdateRecord.write(record).getJson();
    Mockito.when(repository.getReferenceById(data.getId())).thenThrow(EntityNotFoundException.class);
    mockMvc.perform(
    put("/destinos/{id}", data.getId())
    .contentType(MediaType.APPLICATION_JSON)
    .content(json))
    .andExpect(status().isNotFound());
    }
    @Test
    void testDeleteStatusOK() throws Exception {
    mockMvc.perform(
    delete("/destinos/{id}", 1L))
    .andExpect(status().isNoContent());
    Mockito.verify(repository).deleteById(1L);
    }
    }

  3. Run tests 🧪

🥅 Exception Handler

@RestControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(value = EntityNotFoundException.class)
public ResponseEntity<String> handleEntityNotFoundException(EntityNotFoundException e) {
return ResponseEntity.notFound().build();
}
@ExceptionHandler(value = ResponseStatusException.class)
public ResponseEntity<String> handleResponseStatusException(ResponseStatusException ex) {
return ResponseEntity.status(ex.getStatusCode()).body(ex.getReason());
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}

3rd STEP

Destination new fields

  1. Create migration file
    alter table destinations
    add photo2 varchar(100) not null,
    add meta varchar(160) not null,
    add description longtext
  2. Update model
    public class Destination {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String photo;
    private String photo2;
    private String meta;
    private String description;
    private BigDecimal Price;
    public Destination(@Valid DestinationCreateRecord record) {
    this.name = record.name();
    this.photo = record.photo();
    this.photo2 = record.photo2();
    this.Price = record.price();
    this.meta = record.meta();
    this.description = record.description();
    }
    public void update(@Valid DestinationUpdateRecord record) {
    this.name = record.name();
    this.photo = record.photo();
    this.photo2 = record.photo2();
    this.meta = record.meta();
    this.description = record.description();
    this.Price = record.price();
    }
    }
  3. Update DestinationCreateRecord
    String photo2,
    @NotBlank
    @Size(max = 160)
    String meta,
    String description,
    @DecimalMin(value = "0.0", inclusive = false)
  4. update DestinationDetailRecord
    public record DestinationDetailRecord(
    Long id,
    String name,
    String photo,
    String photo2,
    String meta,
    String description,
    BigDecimal price
    ) {
    public DestinationDetailRecord(Destination destination) {
    this(destination.getId(), destination.getName(), destination.getPhoto(), destination.getPhoto2(), destination.getMeta(), destination.getDescription(), destination.getPrice());
    }
    }
  5. update DestinationUpdateRecord
    @NotBlank
    String photo2,
    @NotBlank
    @Size(max = 160)
    String meta,
    String description,
  6. update GenerateData
    faker.internet().url(),
    faker.lorem().characters(1, 160, true, true, true),
    faker.lorem().paragraph(2),
  7. Update DestinationControllerUnitTest
    new DestinationUpdateRecord(data.getName(), data.getPhoto(), data.getPhoto2(), data.getMeta(), data.getDescription(), data.getPrice());

AI integration

  1. Add dependency

    milhasapi/pom.xml

    Lines 21 to 25 in e0c9033

    <dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>service</artifactId>
    <version>0.15.0</version>
    </dependency>
  2. create service
    @Service
    public class GptGuideService {
    public String generate(String destination){
    OpenAiService service = new OpenAiService(System.getenv("OPENAI_KEY"));
    List<ChatMessage> messages = new ArrayList<>();
    messages.add(new ChatMessage(ChatMessageRole.USER.value(), "I want you to act as a travel guide. I will write you a location and you will write a 200 character text about this location, it's highlights and unique experiences. My first request sugestion is " + destination));
    ChatCompletionRequest completion = ChatCompletionRequest
    .builder()
    .model("gpt-3.5-turbo")
    .messages(messages)
    .maxTokens(200)
    .build();
    ChatMessage response = service.createChatCompletion(completion).getChoices().get(0).getMessage();
    return response.getContent();
    }
    }
  3. update Destination constructor
    if(description == null || description == ""){
    GptGuideService gpt = new GptGuideService();
    this.description = gpt.generate(this.name);
    }
  4. create environment variable
    export OPENAI_KEY=your_openai_api_key

Reference Documentation

For further reference, please consider the following sections:

Guides

The following guides illustrate how to use some features concretely: