From 5125b782ad27eaedf4561c61e16a809db91eea88 Mon Sep 17 00:00:00 2001 From: jaimeeg5 Date: Fri, 4 Sep 2026 15:47:24 +0200 Subject: [PATCH] feat: add image CRUD --- .../codeurjcstudents/pcmod/dto/ImageDTO.java | 5 + .../pcmod/dto/ImageMapper.java | 15 +++ .../codeurjcstudents/pcmod/model/Image.java | 48 +++++++ .../pcmod/repository/ImageRepository.java | 9 ++ .../pcmod/service/DatabaseInitializer.java | 3 + .../pcmod/service/ImageService.java | 87 +++++++++++++ .../resources/sample_images/i5-12400f.webp | Bin 0 -> 15568 bytes .../resources/sample_images/kingston-nv3.webp | Bin 0 -> 17768 bytes .../integration/ImagesIntegrationTests.java | 122 ++++++++++++++++++ .../pcmod/unit/ImagesUnitTests.java | 120 +++++++++++++++++ frontend/app/dtos/ImageDTO.ts | 3 + 11 files changed, 412 insertions(+) create mode 100644 backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageDTO.java create mode 100644 backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageMapper.java create mode 100644 backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Image.java create mode 100644 backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/repository/ImageRepository.java create mode 100644 backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ImageService.java create mode 100644 backend/pcmod/src/main/resources/sample_images/i5-12400f.webp create mode 100644 backend/pcmod/src/main/resources/sample_images/kingston-nv3.webp create mode 100644 backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ImagesIntegrationTests.java create mode 100644 backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ImagesUnitTests.java create mode 100644 frontend/app/dtos/ImageDTO.ts diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageDTO.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageDTO.java new file mode 100644 index 0000000..ad10e83 --- /dev/null +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageDTO.java @@ -0,0 +1,5 @@ +package es.codeurjcstudents.pcmod.dto; + +public record ImageDTO( + Long id) { +} \ No newline at end of file diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageMapper.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageMapper.java new file mode 100644 index 0000000..3559ecf --- /dev/null +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/dto/ImageMapper.java @@ -0,0 +1,15 @@ +package es.codeurjcstudents.pcmod.dto; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import es.codeurjcstudents.pcmod.model.Image; + +@Mapper(componentModel = "spring") +public interface ImageMapper { + + ImageDTO toDTO(Image image); + + @Mapping(target = "imageFile", ignore = true) + Image toDomain(ImageDTO imageDTO); +} \ No newline at end of file diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Image.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Image.java new file mode 100644 index 0000000..7447d6e --- /dev/null +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Image.java @@ -0,0 +1,48 @@ +package es.codeurjcstudents.pcmod.model; + +import java.sql.Blob; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; + +@Entity +public class Image { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Lob + private Blob imageFile; + + public Image() { + } + + public Image(Blob imageFile) { + this.imageFile = imageFile; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Blob getImageFile() { + return imageFile; + } + + public void setImageFile(Blob imageFile) { + this.imageFile = imageFile; + } + + @Override + public String toString() { + return "Image [id=" + id + "]"; + } +} \ No newline at end of file diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/repository/ImageRepository.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/repository/ImageRepository.java new file mode 100644 index 0000000..f24d2e0 --- /dev/null +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/repository/ImageRepository.java @@ -0,0 +1,9 @@ +package es.codeurjcstudents.pcmod.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import es.codeurjcstudents.pcmod.model.Image; + +public interface ImageRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java index 0f45894..3024fac 100644 --- a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java @@ -16,6 +16,9 @@ public class DatabaseInitializer { @Autowired private ComponentsRepository componentsRepository; + @Autowired + private ImageService imageService; + @PostConstruct public void init() { diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ImageService.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ImageService.java new file mode 100644 index 0000000..2113a9d --- /dev/null +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ImageService.java @@ -0,0 +1,87 @@ +package es.codeurjcstudents.pcmod.service; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import javax.sql.rowset.serial.SerialBlob; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import es.codeurjcstudents.pcmod.dto.ComponentMapper; +import es.codeurjcstudents.pcmod.model.Image; +import es.codeurjcstudents.pcmod.repository.ImageRepository; + +@Service +public class ImageService { + + private static final long MAX_SIZE = 10 * 1024 * 1024; // 10MB + private static final List ALLOWED_TYPES = List.of("image/jpeg", "image/png", "image/webp"); + + private final ImageRepository imageRepository; + + @Autowired + private ComponentMapper componentMapper; + + public ImageService(ImageRepository imageRepository) { + this.imageRepository = imageRepository; + } + + public List findAll() { + return imageRepository.findAll(); + } + + public Image createImage(InputStream inputStream) throws IOException { + + Image image = new Image(); + + try { + image.setImageFile(new SerialBlob(inputStream.readAllBytes())); + } catch (Exception e) { + throw new IOException("Failed to create image", e); + } + + imageRepository.save(image); + + return image; + } + + public Image replaceImageFile(long id, InputStream inputStream) throws IOException { + + Image image = imageRepository.findById(id).orElseThrow(); + + try { + image.setImageFile(new SerialBlob(inputStream.readAllBytes())); + } catch (Exception e) { + throw new IOException("Failed to create image", e); + } + + imageRepository.save(image); + + return image; + } + + public Image deleteImage(long id) { + + Image image = imageRepository.findById(id).orElseThrow(); + imageRepository.deleteById(id); + + return image; + } + + public void validate(MultipartFile imageField) { + + if (imageField.getSize() > MAX_SIZE) { + throw new IllegalArgumentException("El tamaño de la imagen no puede superar los 10MB."); + } + + String contentType = imageField.getContentType(); + if (contentType == null || !ALLOWED_TYPES.contains(contentType)) { + throw new IllegalArgumentException( + "El tipo de archivo no es válido. Solo se permiten imágenes JPEG, PNG y WebP."); + } + + } +} \ No newline at end of file diff --git a/backend/pcmod/src/main/resources/sample_images/i5-12400f.webp b/backend/pcmod/src/main/resources/sample_images/i5-12400f.webp new file mode 100644 index 0000000000000000000000000000000000000000..91a2270de5beeaf9cd0a228b8a93e205ba3d0e1d GIT binary patch literal 15568 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 m{h$iQKub0hu5ut>e)C`e0064M0000000000000000000BpTjHw literal 0 HcmV?d00001 diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ImagesIntegrationTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ImagesIntegrationTests.java new file mode 100644 index 0000000..41a930d --- /dev/null +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ImagesIntegrationTests.java @@ -0,0 +1,122 @@ +package es.codeurjcstudents.pcmod.integration; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.io.Resource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import es.codeurjcstudents.pcmod.model.Image; +import es.codeurjcstudents.pcmod.repository.ImageRepository; +import es.codeurjcstudents.pcmod.service.ImageService; + +@Tag("server-integration") +@SpringBootTest +@Testcontainers +public class ImagesIntegrationTests { + + @Container + private static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.4") + .withDatabaseName("TestDB") + .withUsername("TestDBUser") + .withPassword("TestDBPassword"); + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", + () -> mysqlContainer.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true"); + + registry.add("spring.datasource.username", mysqlContainer::getUsername); + registry.add("spring.datasource.password", mysqlContainer::getPassword); + registry.add("spring.datasource.driver-class-name", mysqlContainer::getDriverClassName); + } + + @Autowired + private ImageService imageService; + + @Autowired + private ImageRepository imageRepository; + + @BeforeEach + void setUp() { + imageRepository.deleteAll(); + imageRepository.save(new Image()); + imageRepository.save(new Image()); + imageRepository.save(new Image()); + } + + @Test + void createImages() throws IOException { + + imageRepository.deleteAll(); + + List imageList = imageService.findAll(); + assertEquals(0, imageList.size()); + + Resource imagePath1 = new ClassPathResource("/sample_images/i5-12400f.webp"); + Image image1 = imageService.createImage(imagePath1.getInputStream()); + + Resource imagePath2 = new ClassPathResource("/sample_images/kingston-nv3.webp"); + Image image2 = imageService.createImage(imagePath2.getInputStream()); + + imageList = imageService.findAll(); + assertEquals(2, imageList.size()); + + List idList = imageList.stream().map(Image::getId).toList(); + assertTrue(idList.contains(image1.getId())); + assertTrue(idList.contains(image2.getId())); + + } + + @Test + void replaceImage() throws IOException, SQLException { + + imageRepository.deleteAll(); + + Resource originalPath = new ClassPathResource("/sample_images/kingston-nv3.webp"); + byte[] originalBytes = originalPath.getInputStream().readAllBytes(); + Image image = imageService.createImage(originalPath.getInputStream()); + + Resource replacementPath = new ClassPathResource("/sample_images/i5-12400f.webp"); + byte[] expectedBytes = replacementPath.getInputStream().readAllBytes(); + + Image updatedImage = imageService.replaceImageFile(image.getId(), replacementPath.getInputStream()); + byte[] updatedBytes = updatedImage.getImageFile().getBytes(1, (int) updatedImage.getImageFile().length()); + + assertEquals(image.getId(), updatedImage.getId()); + assertFalse(Arrays.equals(originalBytes, updatedBytes)); + assertArrayEquals(expectedBytes, updatedBytes); + + } + + @Test + void deleteImage() throws IOException { + + Long imageToDelete = imageRepository.findAll().getFirst().getId(); + imageService.deleteImage(imageToDelete); + + List imageList = imageService.findAll(); + assertEquals(2, imageList.size()); + + } + +} \ No newline at end of file diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ImagesUnitTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ImagesUnitTests.java new file mode 100644 index 0000000..896a048 --- /dev/null +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ImagesUnitTests.java @@ -0,0 +1,120 @@ +package es.codeurjcstudents.pcmod.unit; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import org.springframework.core.io.Resource; +import org.springframework.mock.web.MockMultipartFile; + +import es.codeurjcstudents.pcmod.model.Image; +import es.codeurjcstudents.pcmod.repository.ImageRepository; +import es.codeurjcstudents.pcmod.service.ImageService; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Optional; + +import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialException; + +@Tag("server-unit") +public class ImagesUnitTests { + + @Test + public void testCreateImage() throws SerialException, SQLException, IOException { + + ImageRepository imageRepository = mock(ImageRepository.class); + ImageService imageService = new ImageService(imageRepository); + + String classpathResource = "/sample_images/i5-12400f.webp"; + Resource imagePath = new ClassPathResource(classpathResource); + Image actualImage = imageService.createImage(imagePath.getInputStream()); + + Image expectedImage = new Image(); + expectedImage.setImageFile(new SerialBlob(imagePath.getInputStream().readAllBytes())); + + assertEquals(expectedImage.getImageFile(), actualImage.getImageFile()); + verify(imageRepository).save(actualImage); + + } + + @Test + public void testReplaceImage() throws SerialException, SQLException, IOException { + + ImageRepository imageRepository = mock(ImageRepository.class); + ImageService imageService = new ImageService(imageRepository); + + byte[] originalBytes = new byte[] { 1, 2, 3 }; + Image existingImage = new Image(); + existingImage.setImageFile(new SerialBlob(originalBytes)); + when(imageRepository.findById(1L)).thenReturn(Optional.of(existingImage)); + + String classpathResource = "/sample_images/i5-12400f.webp"; + Resource imagePath = new ClassPathResource(classpathResource); + Image updatedImage = imageService.replaceImageFile(1L, imagePath.getInputStream()); + byte[] updatedBytes = updatedImage.getImageFile().getBytes(1, (int) updatedImage.getImageFile().length()); + byte[] expectedBytes = imagePath.getInputStream().readAllBytes(); + + assertEquals(expectedBytes.length, updatedBytes.length); + assertNotEquals(originalBytes, updatedBytes); + verify(imageRepository).save(updatedImage); + + } + + @Test + public void testDeleteImage() throws IOException { + + ImageRepository imageRepository = mock(ImageRepository.class); + ImageService imageService = new ImageService(imageRepository); + + Image imageToDelete = new Image(); + when(imageRepository.findById(1L)).thenReturn(Optional.of(imageToDelete)); + + imageService.deleteImage(1L); + + verify(imageRepository).deleteById(1L); + + } + + @Test + void testValidateImage() { + + ImageRepository imageRepository = mock(ImageRepository.class); + ImageService imageService = new ImageService(imageRepository); + + MockMultipartFile validImage = new MockMultipartFile( + "image", "image.webp", "image/webp", new byte[1024]); + MockMultipartFile invalidTypeImage = new MockMultipartFile( + "image", "image.txt", "text/plain", new byte[1024]); + MockMultipartFile imageWithoutType = new MockMultipartFile( + "image", "image", null, new byte[1024]); + MockMultipartFile oversizedImage = new MockMultipartFile( + "image", "image.webp", "image/webp", new byte[10 * 1024 * 1024 + 1]); + + assertDoesNotThrow(() -> imageService.validate(validImage)); + assertThrows(IllegalArgumentException.class, () -> imageService.validate(invalidTypeImage)); + assertThrows(IllegalArgumentException.class, () -> imageService.validate(imageWithoutType)); + assertThrows(IllegalArgumentException.class, () -> imageService.validate(oversizedImage)); + + } + + @Test + public void testFindAllImages() { + ImageRepository imageRepository = mock(ImageRepository.class); + ImageService imageService = new ImageService(imageRepository); + + imageService.findAll(); + + verify(imageRepository).findAll(); + } + +} diff --git a/frontend/app/dtos/ImageDTO.ts b/frontend/app/dtos/ImageDTO.ts new file mode 100644 index 0000000..b1d0c13 --- /dev/null +++ b/frontend/app/dtos/ImageDTO.ts @@ -0,0 +1,3 @@ +export default interface ImageDTO { + id: number; +} \ No newline at end of file