-
Create a project from
Spring Initializr
- Java
17
- Maven
3.1.2
- Dependecies
- Spring Boot DevTools
- Spring Data JPA
- Lombok
- Flyway Migration
- MySQL Driver
- Java
-
Add extra dependencies
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> -
Initial configuration
milhasapi/src/main/resources/application.properties
Lines 1 to 3 in bce5a45
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
-
Create Database
CREATE DATABASE milhas_api;
-
Run it 🏁
Now you can run the project and access
http://javahost:8080/swagger-ui/index.html
milhasapi/src/main/java/ecureuill/milhasapi/infra/doc/SpringDocConfig.java
Lines 11 to 25 in bce5a45
@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"))); | |
} | |
} |
-
Create table
testimonials
through flyway migration filecreate table testimonials( id bigint not null auto_increment, name varchar(100) not null, testimonial varchar(100) not null, photo varchar(100), primary key(id) ); -
Create entity
milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/Testimonial.java
Lines 13 to 32 in 5004333
@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(); } } -
Create repository
milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/TestimonialRepository.java
Lines 5 to 7 in 5a2c728
public interface TestimonialRepository extends JpaRepository<Testimonial, Long>{ }
-
Create
TestimonialDetailRecord
andTestimonialCreateRecord
milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/TestimonialDetailRecord.java
Lines 3 to 12 in 5a2c728
public record TestimonialDetailRecord( String name, String photo, String testimonial ) { public TestimonialDetailRecord(Testimonial doctor) { this(doctor.getName(), doctor.getPhoto(), doctor.getTestimonial()); } } milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/TestimonialCreateRecord.java
Lines 5 to 13 in 5a2c728
public record TestimonialCreateRecord( @NotBlank String name, String photo, @NotBlank String testimonial ) { } -
Create
depoimentos
endpoint and POST requestmilhasapi/src/main/java/ecureuill/milhasapi/controller/TestimonialController.java
Lines 20 to 38 in 1b4d491
@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); } } -
Run it 🏁
You can check in swagger, the newly created endpoint
-
Create
TestimonialUpdateRecord
milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/TestimonialUpdateRecord.java
Lines 5 to 13 in 5a2c728
public record TestimonialUpdateRecord( @NotBlank String name, @NotBlank String testimonial, String photo ) { } -
Update
TestimonialController
milhasapi/src/main/java/ecureuill/milhasapi/controller/TestimonialController.java
Lines 48 to 77 in 5a2c728
@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(); } -
Run it 🏁
ℹ️ You can check in swagger, the newly created requests verbs
-
Add
findThreeTestimonials
query toTestimonialRepository
milhasapi/src/main/java/ecureuill/milhasapi/domain/testimonial/TestimonialRepository.java
Lines 10 to 15 in 0b7c599
@Query(""" select t from Testimonial t order by rand() limit 3 """) List<Testimonial> findThreeTestimonials(); -
Create
RandomTestimonialController
milhasapi/src/main/java/ecureuill/milhasapi/controller/RandomTestimonialController.java
Lines 15 to 27 in 0b7c599
@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())); } }
-
Add datafaker depedency
Lines 21 to 25 in 5b8313f
<dependency> <groupId>net.datafaker</groupId> <artifactId>datafaker</artifactId> <version>2.0.1</version> </dependency> -
Create fake data
milhasapi/src/test/java/ecureuill/milhasapi/GenerateData.java
Lines 10 to 27 in 5b8313f
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()); } } -
Create
TestimonialControllerUnitTest
milhasapi/src/test/java/ecureuill/milhasapi/controller/TestimonialControllerUnitTest.java
Lines 37 to 247 in 5b8313f
@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()); } } -
Run tests 🧪
-
Fix code 🐛
milhasapi/src/main/java/ecureuill/milhasapi/controller/TestimonialController.java
Lines 52 to 68 in 5b8313f
@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)
-
Run test again 🧪
The only failing test should be
testUpdateStatusCodeNotFound
due to all exception is being handler by Spring that returns status code500
.
public class CorsConfig implements WebMvcConfigurer { | |
@Override | |
public void addCorsMappings(CorsRegistry registry) { | |
registry.addMapping("/**") | |
.allowedOrigins("*") | |
.allowedMethods("*") | |
.allowedHeaders("*") | |
.allowCredentials(true); | |
} | |
} |
Pretty much the same as Testimonials
-
Create table
destinations
through flyway migration filecreate 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) ); -
Create entity
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/Destination.java
Lines 15 to 29 in 6d95e34
@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; } -
Create repository
❇️
Spring Data JPA derived query
List<Destination> findAllByNameStartingWith(String name);
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationRepository.java
Lines 7 to 11 in a584815
public interface DestinationRepository extends JpaRepository<Destination, Long>{ List<Destination> findAllByNameStartingWith(String name); } -
Create records
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationDetailRecord.java
Lines 5 to 19 in a584815
public record DestinationDetailRecord( Long id, String name, String photo, BigDecimal price ) { public DestinationDetailRecord(Destination destination) { this( destination.getId(), destination.getName(), destination.getPhoto(), destination.getPrice() ); } } milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationCreateRecord.java
Lines 9 to 19 in a584815
public record DestinationCreateRecord( @NotBlank String name, @NotBlank String photo, @DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 5, fraction=2) BigDecimal price ) { } milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationUpdateRecord.java
Lines 9 to 19 in a584815
public record DestinationUpdateRecord( @NotBlank String name, @NotBlank String photo, @DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 5, fraction=2) BigDecimal price ) { } -
Create controller
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/Destination.java
Lines 31 to 41 in a584815
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(); } milhasapi/src/main/java/ecureuill/milhasapi/controller/DestinationController.java
Lines 30 to 97 in a584815
@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);
-
Create fake data
milhasapi/src/test/java/ecureuill/milhasapi/GenerateData.java
Lines 29 to 43 in a584815
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()); } -
Create
DestinationControllerUnitTest
milhasapi/src/test/java/ecureuill/milhasapi/controller/DestinationControllerTest.java
Lines 33 to 212 in a584815
@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); } } -
Run tests 🧪
milhasapi/src/main/java/ecureuill/milhasapi/infra/expection/ControllerAdvice.java
Lines 10 to 27 in a87a4c5
@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()); | |
} | |
} |
- Create migration file
alter table destinations add photo2 varchar(100) not null, add meta varchar(160) not null, add description longtext - Update model
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/Destination.java
Lines 22 to 51 in 3c60e9e
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(); } } - Update DestinationCreateRecord
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationCreateRecord.java
Lines 16 to 21 in 3c60e9e
String photo2, @NotBlank @Size(max = 160) String meta, String description, @DecimalMin(value = "0.0", inclusive = false) - update DestinationDetailRecord
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationDetailRecord.java
Lines 5 to 17 in 3c60e9e
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()); } } - update DestinationUpdateRecord
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/DestinationUpdateRecord.java
Lines 15 to 20 in 3c60e9e
@NotBlank String photo2, @NotBlank @Size(max = 160) String meta, String description, - update GenerateData
milhasapi/src/test/java/ecureuill/milhasapi/GenerateData.java
Lines 35 to 37 in 3c60e9e
faker.internet().url(), faker.lorem().characters(1, 160, true, true, true), faker.lorem().paragraph(2), - Update DestinationControllerUnitTest
new DestinationUpdateRecord(data.getName(), data.getPhoto(), data.getPhoto2(), data.getMeta(), data.getDescription(), data.getPrice());
- Add dependency
Lines 21 to 25 in e0c9033
<dependency> <groupId>com.theokanning.openai-gpt3-java</groupId> <artifactId>service</artifactId> <version>0.15.0</version> </dependency> - create service
milhasapi/src/main/java/ecureuill/milhasapi/infra/openai/GptGuideService.java
Lines 13 to 34 in e0c9033
@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(); } } - update Destination constructor
milhasapi/src/main/java/ecureuill/milhasapi/domain/destination/Destination.java
Lines 43 to 46 in e0c9033
if(description == null || description == ""){ GptGuideService gpt = new GptGuideService(); this.description = gpt.generate(this.name); } - create environment variable
export OPENAI_KEY=your_openai_api_key
For further reference, please consider the following sections:
- Official Apache Maven documentation
- Spring Boot Maven Plugin Reference Guide
- Create an OCI image
- Spring Boot DevTools
- Spring Data JPA
- Flyway Migration
- Openai-java
The following guides illustrate how to use some features concretely: