diff --git a/compose.yaml b/compose.yaml index 972b861..88a960c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,5 +10,14 @@ services: volumes: - postgres_data:/var/lib/postgresql/data # Persists database data + redis: + image: redis:7-alpine + ports: + - '6379:6379' + volumes: + - redis_data:/data + command: redis-server --appendonly yes + volumes: - postgres_data: \ No newline at end of file + postgres_data: + redis_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index 59fe667..8ec1cd8 100644 --- a/pom.xml +++ b/pom.xml @@ -155,20 +155,31 @@ cloudinary-http5 2.3.0 + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate6 + org.flywaydb flyway-core 10.10.0 + org.flywaydb flyway-database-postgresql 10.10.0 - + - + diff --git a/src/main/java/ua/com/javarush/gnew/contactm/ContactManagerSpringBootApplication.java b/src/main/java/ua/com/javarush/gnew/contactm/ContactManagerSpringBootApplication.java index 85d1e7e..8920d0b 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/ContactManagerSpringBootApplication.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/ContactManagerSpringBootApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication +@EnableCaching public class ContactManagerSpringBootApplication { public static void main(String[] args) { diff --git a/src/main/java/ua/com/javarush/gnew/contactm/DTOs/ContactCacheDto.java b/src/main/java/ua/com/javarush/gnew/contactm/DTOs/ContactCacheDto.java new file mode 100644 index 0000000..7d75ac0 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/contactm/DTOs/ContactCacheDto.java @@ -0,0 +1,25 @@ +package ua.com.javarush.gnew.contactm.DTOs; + +import java.util.Date; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ContactCacheDto { + private long id; + private String name; + private String lastName; + private String imageUrl; + private Date createDate; + private Date modifyDate; + private List emailAddresses; + private List phoneNumbers; + private List socialNetworks; + private String contactBookName; +} diff --git a/src/main/java/ua/com/javarush/gnew/contactm/component/RedisHealthCheck.java b/src/main/java/ua/com/javarush/gnew/contactm/component/RedisHealthCheck.java new file mode 100644 index 0000000..a90de18 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/contactm/component/RedisHealthCheck.java @@ -0,0 +1,26 @@ +package ua.com.javarush.gnew.contactm.component; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RedisHealthCheck { + + private final RedisTemplate redisTemplate; + + @PostConstruct + public void checkRedisConnection() { + try { + String pong = redisTemplate.getConnectionFactory().getConnection().ping(); + log.info("Redis connection successful. Response: {}", pong); + } catch (Exception e) { + log.warn("Redis connection failed: {}", e.getMessage()); + log.warn("Application will continue without Redis caching"); + } + } +} diff --git a/src/main/java/ua/com/javarush/gnew/contactm/config/RedisConfig.java b/src/main/java/ua/com/javarush/gnew/contactm/config/RedisConfig.java new file mode 100644 index 0000000..9440538 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/contactm/config/RedisConfig.java @@ -0,0 +1,74 @@ +package ua.com.javarush.gnew.contactm.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class RedisConfig { + + /** Shared ObjectMapper with Hibernate support. */ + @Bean + public ObjectMapper redisObjectMapper() { + Hibernate6Module hibernateModule = new Hibernate6Module(); + hibernateModule.configure(Hibernate6Module.Feature.FORCE_LAZY_LOADING, false); + hibernateModule.configure( + Hibernate6Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true); + + return new ObjectMapper().registerModule(hibernateModule); + } + + /** Generic JSON serializer using the shared ObjectMapper. */ + @Bean + public RedisSerializer genericJsonSerializer(ObjectMapper redisObjectMapper) { + return new GenericJackson2JsonRedisSerializer(redisObjectMapper); + } + + /** A RedisTemplate that uses String keys and JSON‐serialized values. */ + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory connectionFactory, RedisSerializer genericJsonSerializer) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // key serializers + StringRedisSerializer stringSerializer = new StringRedisSerializer(); + template.setKeySerializer(stringSerializer); + template.setHashKeySerializer(stringSerializer); + + // value serializers + template.setValueSerializer(genericJsonSerializer); + template.setHashValueSerializer(genericJsonSerializer); + + template.afterPropertiesSet(); + log.info("RedisTemplate initialized"); + return template; + } + + /** RedisCacheManager that applies a 60‐minute TTL and JSON serialization. */ + @Bean + public RedisCacheManager cacheManager( + RedisConnectionFactory connectionFactory, RedisSerializer genericJsonSerializer) { + RedisCacheConfiguration cacheConfig = + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(60)) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(genericJsonSerializer)); + + return RedisCacheManager.builder(connectionFactory).cacheDefaults(cacheConfig).build(); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApi.java b/src/main/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApi.java index ad96d67..eb830b2 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApi.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApi.java @@ -10,14 +10,14 @@ import ua.com.javarush.gnew.contactm.DTOs.ContactDTO; import ua.com.javarush.gnew.contactm.entity.Contact; import ua.com.javarush.gnew.contactm.mapper.ContactMapper; -import ua.com.javarush.gnew.contactm.repository.ContactRepository; +import ua.com.javarush.gnew.contactm.services.ContactService; @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/contact") public class ContactControllerApi { - private final ContactRepository contactRepository; + private final ContactService contactService; private final ContactMapper contactMapper; @PreAuthorize("hasRole('USER')") @@ -35,7 +35,7 @@ public String adminEndpoint() { @GetMapping public ResponseEntity getContact(@RequestParam("id") Long id) { log.debug("getContact: id={}", id); - return contactRepository + return contactService .findById(id) .map(contact -> new ResponseEntity<>(contactMapper.toDto(contact), HttpStatus.OK)) .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); @@ -44,14 +44,14 @@ public ResponseEntity getContact(@RequestParam("id") Long id) { @PostMapping public ResponseEntity save(@RequestBody ContactDTO contactDTO) { Contact contact = contactMapper.toEntity(contactDTO); - Contact saved = contactRepository.save(contact); + Contact saved = contactService.save(contact); return new ResponseEntity<>(contactMapper.toDto(saved), HttpStatus.CREATED); } @PutMapping public ResponseEntity update( @RequestParam("id") Long id, @RequestBody ContactDTO contactDTO) { - Optional existingOpt = contactRepository.findById(id); + Optional existingOpt = contactService.findById(id); if (existingOpt.isEmpty()) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } @@ -59,14 +59,14 @@ public ResponseEntity update( Contact contactToUpdate = contactMapper.toEntity(contactDTO); contactToUpdate.setId(id); - Contact saved = contactRepository.save(contactToUpdate); + Contact saved = contactService.save(contactToUpdate); return new ResponseEntity<>(contactMapper.toDto(saved), HttpStatus.OK); } @DeleteMapping public ResponseEntity delete(@RequestParam("id") Long id) { - if (contactRepository.existsById(id)) { - contactRepository.deleteById(id); + if (contactService.existsById(id)) { + contactService.deleteById(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); diff --git a/src/main/java/ua/com/javarush/gnew/contactm/controller/web/ContactController.java b/src/main/java/ua/com/javarush/gnew/contactm/controller/web/ContactController.java index d4f1edd..380697c 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/controller/web/ContactController.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/controller/web/ContactController.java @@ -1,6 +1,7 @@ package ua.com.javarush.gnew.contactm.controller.web; import java.io.IOException; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -11,6 +12,7 @@ import ua.com.javarush.gnew.contactm.DTOs.EmailDTO; import ua.com.javarush.gnew.contactm.DTOs.PhoneDTO; import ua.com.javarush.gnew.contactm.DTOs.SocialNetworkDTO; +import ua.com.javarush.gnew.contactm.entity.Contact; import ua.com.javarush.gnew.contactm.mapper.ContactMapper; import ua.com.javarush.gnew.contactm.services.CloudinaryService; import ua.com.javarush.gnew.contactm.services.ContactService; @@ -26,8 +28,13 @@ public class ContactController { @GetMapping("/edit/{id}") public String edit(@PathVariable Long id, Model model) { + Optional byId = contactService.findById(id); + + // TODO: 404 if not found + Contact contact = byId.get(); + + ContactDTO dto = contactMapper.toDto(contact); - ContactDTO dto = contactMapper.toDto(contactService.findById(id)); if (dto.getEmails().isEmpty()) dto.getEmails().add(new EmailDTO()); if (dto.getPhones().isEmpty()) dto.getPhones().add(new PhoneDTO()); if (dto.getNetworks().isEmpty()) dto.getNetworks().add(new SocialNetworkDTO()); diff --git a/src/main/java/ua/com/javarush/gnew/contactm/controller/web/HomeController.java b/src/main/java/ua/com/javarush/gnew/contactm/controller/web/HomeController.java index d639330..0d4cd20 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/controller/web/HomeController.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/controller/web/HomeController.java @@ -1,24 +1,24 @@ package ua.com.javarush.gnew.contactm.controller.web; import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import ua.com.javarush.gnew.contactm.entity.Contact; -import ua.com.javarush.gnew.contactm.repository.ContactRepository; +import ua.com.javarush.gnew.contactm.services.ContactService; @Controller +@RequiredArgsConstructor +@Slf4j public class HomeController { - private final ContactRepository contactRepository; - - public HomeController(ContactRepository contactRepository) { - this.contactRepository = contactRepository; - } + private final ContactService contactService; @GetMapping public String home(Model model) { - List all = contactRepository.findAll(); + List all = contactService.findAll(); model.addAttribute("tableName", "All contacts"); model.addAttribute("contacts", all); return "home"; diff --git a/src/main/java/ua/com/javarush/gnew/contactm/entity/Contact.java b/src/main/java/ua/com/javarush/gnew/contactm/entity/Contact.java index 7b8dc67..26a3934 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/entity/Contact.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/entity/Contact.java @@ -1,5 +1,7 @@ package ua.com.javarush.gnew.contactm.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; import com.google.gson.annotations.Expose; import jakarta.persistence.*; import java.util.ArrayList; @@ -26,6 +28,7 @@ public class Contact { @ManyToOne @JoinColumn(name = "contact_book_id") + @JsonBackReference private ContactBook contactBook; @Column(name = "name") @@ -35,18 +38,21 @@ public class Contact { // Consider switching to LAZY loading if appropriate. @OneToMany(mappedBy = "contact", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Expose + @JsonManagedReference private List emails = new ArrayList<>(); @Column(name = "last_name") @Expose private String lastName; - @OneToMany(mappedBy = "contact", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @OneToMany(mappedBy = "contact", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Expose + @JsonManagedReference private List phones = new ArrayList<>(); @OneToMany(mappedBy = "contact", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Expose + @JsonManagedReference private List networks = new ArrayList<>(); @Expose private String imageUrl; diff --git a/src/main/java/ua/com/javarush/gnew/contactm/entity/ContactBook.java b/src/main/java/ua/com/javarush/gnew/contactm/entity/ContactBook.java index b6aaea4..6bfaf9b 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/entity/ContactBook.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/entity/ContactBook.java @@ -1,5 +1,6 @@ package ua.com.javarush.gnew.contactm.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; import com.google.gson.annotations.Expose; import jakarta.persistence.*; import java.util.Date; @@ -33,6 +34,7 @@ public class ContactBook { @OneToMany(mappedBy = "contactBook", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Expose + @JsonManagedReference private List contacts; @CreationTimestamp diff --git a/src/main/java/ua/com/javarush/gnew/contactm/entity/Email.java b/src/main/java/ua/com/javarush/gnew/contactm/entity/Email.java index 86926a6..52e9a94 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/entity/Email.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/entity/Email.java @@ -1,5 +1,6 @@ package ua.com.javarush.gnew.contactm.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.google.gson.annotations.Expose; import jakarta.persistence.*; import lombok.Getter; @@ -26,6 +27,7 @@ public class Email { @ManyToOne(fetch = FetchType.EAGER, optional = true) @JoinColumn(name = "contact_id", nullable = true) + @JsonBackReference private Contact contact; @Override diff --git a/src/main/java/ua/com/javarush/gnew/contactm/entity/Phone.java b/src/main/java/ua/com/javarush/gnew/contactm/entity/Phone.java index 320138d..77ff492 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/entity/Phone.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/entity/Phone.java @@ -1,5 +1,6 @@ package ua.com.javarush.gnew.contactm.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.google.gson.annotations.Expose; import jakarta.persistence.*; import lombok.Getter; @@ -27,6 +28,7 @@ public class Phone { @ManyToOne(fetch = FetchType.LAZY, optional = true) @JoinColumn(name = "contact_id", nullable = true) + @JsonBackReference private Contact contact; @Override diff --git a/src/main/java/ua/com/javarush/gnew/contactm/entity/SocialNetwork.java b/src/main/java/ua/com/javarush/gnew/contactm/entity/SocialNetwork.java index f7f7558..ef17375 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/entity/SocialNetwork.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/entity/SocialNetwork.java @@ -1,5 +1,6 @@ package ua.com.javarush.gnew.contactm.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.google.gson.annotations.Expose; import jakarta.persistence.*; import lombok.Getter; @@ -24,6 +25,7 @@ public class SocialNetwork { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "contact_id", nullable = true) + @JsonBackReference private Contact contact; @Override diff --git a/src/main/java/ua/com/javarush/gnew/contactm/mapper/ContactCacheMapper.java b/src/main/java/ua/com/javarush/gnew/contactm/mapper/ContactCacheMapper.java new file mode 100644 index 0000000..83d87f5 --- /dev/null +++ b/src/main/java/ua/com/javarush/gnew/contactm/mapper/ContactCacheMapper.java @@ -0,0 +1,48 @@ +package ua.com.javarush.gnew.contactm.mapper; + +import java.util.List; +import java.util.stream.Collectors; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import ua.com.javarush.gnew.contactm.DTOs.ContactCacheDto; +import ua.com.javarush.gnew.contactm.entity.Contact; +import ua.com.javarush.gnew.contactm.entity.Email; +import ua.com.javarush.gnew.contactm.entity.Phone; +import ua.com.javarush.gnew.contactm.entity.SocialNetwork; + +@Mapper(componentModel = "spring") +public interface ContactCacheMapper { + + @Mapping(target = "emailAddresses", source = "emails", qualifiedByName = "emailsToStrings") + @Mapping(target = "phoneNumbers", source = "phones", qualifiedByName = "phonesToStrings") + @Mapping(target = "socialNetworks", source = "networks", qualifiedByName = "networksToStrings") + @Mapping(target = "contactBookName", source = "contactBook.name") + ContactCacheDto toDto(Contact contact); + + List toDtoList(List contacts); + + @Named("emailsToStrings") + default List emailsToStrings(List emails) { + if (emails == null) { + return List.of(); + } + return emails.stream().map(Email::getEmail).collect(Collectors.toList()); + } + + @Named("phonesToStrings") + default List phonesToStrings(List phones) { + if (phones == null) { + return List.of(); + } + return phones.stream().map(Phone::getPhone).collect(Collectors.toList()); + } + + @Named("networksToStrings") + default List networksToStrings(List networks) { + if (networks == null) { + return List.of(); + } + return networks.stream().map(SocialNetwork::getAccount).collect(Collectors.toList()); + } +} diff --git a/src/main/java/ua/com/javarush/gnew/contactm/repository/ContactRepository.java b/src/main/java/ua/com/javarush/gnew/contactm/repository/ContactRepository.java index 515e448..4614513 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/repository/ContactRepository.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/repository/ContactRepository.java @@ -1,6 +1,9 @@ package ua.com.javarush.gnew.contactm.repository; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import ua.com.javarush.gnew.contactm.entity.Contact; -public interface ContactRepository extends JpaRepository {} +public interface ContactRepository extends JpaRepository { + List findAllByNameContainingIgnoreCase(String name); +} diff --git a/src/main/java/ua/com/javarush/gnew/contactm/services/ContactService.java b/src/main/java/ua/com/javarush/gnew/contactm/services/ContactService.java index ddd8c35..3cc99d3 100644 --- a/src/main/java/ua/com/javarush/gnew/contactm/services/ContactService.java +++ b/src/main/java/ua/com/javarush/gnew/contactm/services/ContactService.java @@ -1,6 +1,12 @@ package ua.com.javarush.gnew.contactm.services; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import ua.com.javarush.gnew.contactm.DTOs.ContactDTO; import ua.com.javarush.gnew.contactm.entity.Contact; @@ -9,24 +15,60 @@ @Service @RequiredArgsConstructor +@CacheConfig(cacheNames = "contacts") // all methods will use the "contacts" cache by default public class ContactService { private final ContactRepository contactRepository; private final ContactMapper contactMapper; - public void save(Contact contact) { - contactRepository.save(contact); + /** Save a Contact entity and update the cache entry for it. */ + @CachePut(key = "#contact.id") + public Contact save(Contact contact) { + return contactRepository.save(contact); } - public void save(ContactDTO contact) { - contactRepository.save(contactMapper.toEntity(contact)); + /** Save via DTO and update the cache entry for the resulting Contact. */ + @CachePut(key = "#result.id") + public Contact save(ContactDTO contactDto) { + Contact contact = contactMapper.toEntity(contactDto); + return contactRepository.save(contact); } - public Contact findById(Long id) { - return contactRepository.findById(id).orElse(null); + /** + * Read-through cache: will return from cache if present, otherwise load from DB and cache it. + * Returns Optional for better null handling. + */ + @Cacheable(key = "#id") + public Optional findById(Long id) { + return contactRepository.findById(id); } + /** Check if a contact exists by ID. */ + @Cacheable(key = "'exists-' + #id") + public boolean existsById(Long id) { + return contactRepository.existsById(id); + } + + /** Delete from the DB and evict the cache entry. */ + @CacheEvict(key = "#id") public void delete(Long id) { contactRepository.deleteById(id); } + + /** Delete by ID from the DB and evict the cache entry. */ + @CacheEvict(key = "#id") + public void deleteById(Long id) { + contactRepository.deleteById(id); + } + + @Cacheable(key = "'all'") + public List findAll() { + return contactRepository.findAll(); + } + + // find all by name + @Cacheable(key = "#name") + public Iterable findAllByName(String name) { + return contactRepository.findAllByNameContainingIgnoreCase(name); + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 031ba8e..e09023e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -7,10 +7,22 @@ spring: jpa: hibernate: ddl-auto: update + data: + redis: + host: ${REDIS_HOST:localhost} # falls back to localhost if not set + port: ${REDIS_PORT:6379} # falls back to 6379 + password: ${REDIS_PASSWORD:} # empty if not set + ssl: + enabled: ${REDIS_SSL:false} # enable SSL if you set REDIS_SSL=true + lettuce: + pool: + max-active: ${REDIS_MAX_ACTIVE:8} + max-idle: ${REDIS_MAX_IDLE:8} + min-idle: ${REDIS_MIN_IDLE:0} show-sql: true properties: hibernate: - format_sql: true + format_sql: true logging: level: ua.com.javarush.gnew.contactm: DEBUG diff --git a/src/test/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApiTest.java b/src/test/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApiTest.java index b0dddd0..fbd9455 100644 --- a/src/test/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApiTest.java +++ b/src/test/java/ua/com/javarush/gnew/contactm/controller/rest/ContactControllerApiTest.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @@ -16,7 +17,7 @@ import ua.com.javarush.gnew.contactm.DTOs.ContactDTO; import ua.com.javarush.gnew.contactm.entity.Contact; import ua.com.javarush.gnew.contactm.mapper.ContactMapper; -import ua.com.javarush.gnew.contactm.repository.ContactRepository; +import ua.com.javarush.gnew.contactm.services.ContactService; @WebMvcTest( value = ContactControllerApi.class, @@ -25,7 +26,7 @@ class ContactControllerApiTest { @Autowired private MockMvc mvc; - @MockitoBean private ContactRepository contactRepository; + @MockitoBean private ContactService contactService; @MockitoBean private ContactMapper contactMapper; @@ -39,7 +40,7 @@ void getContact_ShouldReturnContactDTOAndStatus200WhenContactExists() throws Exc ContactDTO contactDTO = ContactDTO.builder().id(id).name(name).build(); - when(contactRepository.findById(id)).thenReturn(java.util.Optional.of(contact)); + when(contactService.findById(id)).thenReturn(Optional.ofNullable(contact)); when(contactMapper.toDto(contact)).thenReturn(contactDTO); // Act & Assert @@ -51,4 +52,18 @@ void getContact_ShouldReturnContactDTOAndStatus200WhenContactExists() throws Exc .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(id)) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value(name)); } + + @Test + void getContact_ShouldReturnStatus404WhenContactNotExists() throws Exception { + // Arrange + long id = 1L; + + when(contactService.findById(id)).thenReturn(Optional.empty()); + + // Act & Assert + String path = "/api/v1/contact"; + + mvc.perform(get(path).param("id", "1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } }