From 27535e063c7aa4b2ce0cc5455a906142646284fa Mon Sep 17 00:00:00 2001 From: Morph01 Date: Wed, 3 Dec 2025 19:36:02 +0100 Subject: [PATCH] feat: add tests --- .../CascadeDeleteIntegrationTest.java | 297 ++++++++++++++++ .../PhotoUploadIntegrationTest.java | 288 ++++++++++++++++ .../fotosharing/mapper/PhotoMapperTest.java | 153 +++++++++ .../security/SecurityServiceTest.java | 319 ++++++++++++++++++ target/FotoSharing-0.0.1-SNAPSHOT.war | Bin 61677273 -> 61677420 bytes .../FotoSharing-0.0.1-SNAPSHOT.war.original | Bin 51126359 -> 51126506 bytes 6 files changed, 1057 insertions(+) create mode 100644 src/test/java/local/epul4a/fotosharing/integration/CascadeDeleteIntegrationTest.java create mode 100644 src/test/java/local/epul4a/fotosharing/integration/PhotoUploadIntegrationTest.java create mode 100644 src/test/java/local/epul4a/fotosharing/mapper/PhotoMapperTest.java create mode 100644 src/test/java/local/epul4a/fotosharing/security/SecurityServiceTest.java diff --git a/src/test/java/local/epul4a/fotosharing/integration/CascadeDeleteIntegrationTest.java b/src/test/java/local/epul4a/fotosharing/integration/CascadeDeleteIntegrationTest.java new file mode 100644 index 0000000..eb32461 --- /dev/null +++ b/src/test/java/local/epul4a/fotosharing/integration/CascadeDeleteIntegrationTest.java @@ -0,0 +1,297 @@ +package local.epul4a.fotosharing.integration; + +import local.epul4a.fotosharing.model.Commentaire; +import local.epul4a.fotosharing.model.Photo; +import local.epul4a.fotosharing.model.Utilisateur; +import local.epul4a.fotosharing.repository.CommentaireRepository; +import local.epul4a.fotosharing.repository.PhotoRepository; +import local.epul4a.fotosharing.repository.UtilisateurRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests d'intégration pour la suppression en cascade + * Teste : Supprimer un user doit supprimer ses photos et commentaires + */ +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class CascadeDeleteIntegrationTest { + + @Autowired + private UtilisateurRepository utilisateurRepository; + + @Autowired + private PhotoRepository photoRepository; + + @Autowired + private CommentaireRepository commentaireRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + private Utilisateur userA; + private Utilisateur userB; + + @BeforeEach + void setUp() { + // Nettoyer la BDD + commentaireRepository.deleteAll(); + photoRepository.deleteAll(); + utilisateurRepository.deleteAll(); + + // Créer User A + userA = new Utilisateur(); + userA.setEmail("userA@example.com"); + userA.setNom("A"); + userA.setPrenom("User"); + userA.setPassword(passwordEncoder.encode("password")); + userA = utilisateurRepository.save(userA); + + // Créer User B + userB = new Utilisateur(); + userB.setEmail("userB@example.com"); + userB.setNom("B"); + userB.setPrenom("User"); + userB.setPassword(passwordEncoder.encode("password")); + userB = utilisateurRepository.save(userB); + } + + @Test + void testDeleteUser_ShouldDeleteUserOwnPhotos() { + // Given - User A possède 3 photos + Photo photo1 = createPhoto("photo1.jpg", userA); + Photo photo2 = createPhoto("photo2.jpg", userA); + Photo photo3 = createPhoto("photo3.jpg", userA); + + photoRepository.save(photo1); + photoRepository.save(photo2); + photoRepository.save(photo3); + + // Given - User B possède 2 photos (ne doivent PAS être supprimées) + Photo photoB1 = createPhoto("photoB1.jpg", userB); + Photo photoB2 = createPhoto("photoB2.jpg", userB); + photoRepository.save(photoB1); + photoRepository.save(photoB2); + + // Vérifier la situation initiale + assertEquals(5, photoRepository.count(), "5 photos au total"); + assertEquals(3, photoRepository.findByProprietaire_Email(userA.getEmail()).size()); + assertEquals(2, photoRepository.findByProprietaire_Email(userB.getEmail()).size()); + + // When - Supprimer User A + utilisateurRepository.delete(userA); + utilisateurRepository.flush(); + + // Then - Les photos de User A doivent être supprimées + assertEquals(2, photoRepository.count(), + "Seules les 2 photos de User B doivent rester"); + + List remainingPhotos = photoRepository.findAll(); + assertEquals(2, remainingPhotos.size()); + assertTrue(remainingPhotos.stream().allMatch(p -> p.getProprietaire().getId().equals(userB.getId())), + "Toutes les photos restantes doivent appartenir à User B"); + } + + @Test + void testDeleteUser_ShouldDeleteUserOwnComments() { + // Given - Photo de User B + Photo photoB = createPhoto("photoB.jpg", userB); + photoB = photoRepository.save(photoB); + + // Given - User A commente la photo de User B + Commentaire comment1 = createCommentaire("Super photo !", userA, photoB); + Commentaire comment2 = createCommentaire("J'adore !", userA, photoB); + Commentaire comment3 = createCommentaire("Merci !", userB, photoB); // Commentaire de User B + + commentaireRepository.save(comment1); + commentaireRepository.save(comment2); + commentaireRepository.save(comment3); + + // Vérifier la situation initiale + assertEquals(3, commentaireRepository.count()); + + // When - Supprimer User A + utilisateurRepository.delete(userA); + utilisateurRepository.flush(); + + // Then - Les commentaires de User A doivent être supprimés + assertEquals(1, commentaireRepository.count(), + "Seul le commentaire de User B doit rester"); + + List remainingComments = commentaireRepository.findAll(); + assertEquals(1, remainingComments.size()); + assertEquals(userB.getId(), remainingComments.get(0).getAuteur().getId(), + "Le commentaire restant doit être celui de User B"); + } + + @Test + void testDeleteUser_ShouldDeletePhotosAndAllCommentsOnThosePhotos() { + // Given - User A possède une photo + Photo photoA = createPhoto("photoA.jpg", userA); + photoA = photoRepository.save(photoA); + + // Given - Plusieurs utilisateurs commentent la photo de User A + Commentaire commentByA = createCommentaire("Ma photo", userA, photoA); + Commentaire commentByB = createCommentaire("Belle photo", userB, photoA); + + commentaireRepository.save(commentByA); + commentaireRepository.save(commentByB); + + // Vérifier la situation initiale + assertEquals(1, photoRepository.count()); + assertEquals(2, commentaireRepository.count()); + + // When - Supprimer User A (et donc sa photo) + utilisateurRepository.delete(userA); + utilisateurRepository.flush(); + + // Then - La photo ET tous ses commentaires doivent être supprimés + assertEquals(0, photoRepository.count(), + "La photo de User A doit être supprimée"); + + assertEquals(0, commentaireRepository.count(), + "TOUS les commentaires sur la photo supprimée doivent disparaître (cascade)"); + } + + @Test + void testDeleteUser_ComplexScenario() { + // Scénario complexe : + // - User A a 2 photos + // - User B a 1 photo + // - User A commente sa propre photo ET la photo de User B + // - User B commente sa propre photo ET une photo de User A + + // Given - Photos + Photo photoA1 = photoRepository.save(createPhoto("photoA1.jpg", userA)); + Photo photoA2 = photoRepository.save(createPhoto("photoA2.jpg", userA)); + Photo photoB = photoRepository.save(createPhoto("photoB.jpg", userB)); + + // Given - Commentaires + commentaireRepository.save(createCommentaire("A sur A1", userA, photoA1)); + commentaireRepository.save(createCommentaire("A sur A2", userA, photoA2)); + commentaireRepository.save(createCommentaire("A sur B", userA, photoB)); + commentaireRepository.save(createCommentaire("B sur A1", userB, photoA1)); + commentaireRepository.save(createCommentaire("B sur B", userB, photoB)); + + // Vérifier la situation initiale + assertEquals(3, photoRepository.count()); + assertEquals(5, commentaireRepository.count()); + + // When - Supprimer User A + utilisateurRepository.delete(userA); + utilisateurRepository.flush(); + + // Then - Vérifier les suppressions + assertEquals(1, photoRepository.count(), + "Seule la photo de User B doit rester"); + + List remainingPhotos = photoRepository.findAll(); + assertEquals(photoB.getId(), remainingPhotos.get(0).getId()); + + // Then - Vérifier les commentaires restants + List remainingComments = commentaireRepository.findAll(); + assertEquals(1, remainingComments.size(), + "Seul le commentaire 'B sur B' doit rester"); + + Commentaire lastComment = remainingComments.get(0); + assertEquals("B sur B", lastComment.getTexte()); + assertEquals(userB.getId(), lastComment.getAuteur().getId()); + assertEquals(photoB.getId(), lastComment.getPhoto().getId()); + } + + @Test + void testDeleteUser_ShouldNotAffectOtherUsers() { + // Given - Créer User C + Utilisateur userC = new Utilisateur(); + userC.setEmail("userC@example.com"); + userC.setNom("C"); + userC.setPrenom("User"); + userC.setPassword(passwordEncoder.encode("password")); + userC = utilisateurRepository.save(userC); + + // Given - Chaque utilisateur a des photos et commentaires + Photo photoA = photoRepository.save(createPhoto("photoA.jpg", userA)); + Photo photoB = photoRepository.save(createPhoto("photoB.jpg", userB)); + Photo photoC = photoRepository.save(createPhoto("photoC.jpg", userC)); + + commentaireRepository.save(createCommentaire("Comment A", userA, photoA)); + commentaireRepository.save(createCommentaire("Comment B", userB, photoB)); + commentaireRepository.save(createCommentaire("Comment C", userC, photoC)); + + // Vérifier la situation initiale + assertEquals(3, utilisateurRepository.count()); + assertEquals(3, photoRepository.count()); + assertEquals(3, commentaireRepository.count()); + + // When - Supprimer User A + utilisateurRepository.delete(userA); + utilisateurRepository.flush(); + + // Then - User B et C ne doivent pas être affectés + assertEquals(2, utilisateurRepository.count(), + "Users B et C doivent rester"); + assertEquals(2, photoRepository.count(), + "Photos de B et C doivent rester"); + assertEquals(2, commentaireRepository.count(), + "Commentaires de B et C doivent rester"); + + // Then - Vérifier que les bonnes entités restent + assertTrue(utilisateurRepository.findById(userB.getId()).isPresent()); + assertTrue(utilisateurRepository.findById(userC.getId()).isPresent()); + assertFalse(utilisateurRepository.findById(userA.getId()).isPresent()); + } + + @Test + void testDeleteMultipleUsers_ShouldDeleteRespectiveData() { + // Given - Chaque user a des photos + photoRepository.save(createPhoto("photoA.jpg", userA)); + photoRepository.save(createPhoto("photoB.jpg", userB)); + + assertEquals(2, photoRepository.count()); + + // When - Supprimer les deux utilisateurs + utilisateurRepository.delete(userA); + utilisateurRepository.delete(userB); + utilisateurRepository.flush(); + + // Then - Tout doit être supprimé + assertEquals(0, utilisateurRepository.count()); + assertEquals(0, photoRepository.count()); + } + + // ===================== Méthodes Utilitaires ===================== + + private Photo createPhoto(String filename, Utilisateur owner) { + Photo photo = new Photo(); + photo.setNomFichierOriginal(filename); + photo.setUuidFichier("uuid-" + filename); + photo.setUuidThumbnail("thumb-" + filename); + photo.setVisibilite(Photo.Visibilite.PUBLIC); + photo.setDateUpload(LocalDateTime.now()); + photo.setProprietaire(owner); + photo.setMimeType("image/jpeg"); + photo.setTailleFichier(1024L); + return photo; + } + + private Commentaire createCommentaire(String texte, Utilisateur auteur, Photo photo) { + Commentaire commentaire = new Commentaire(); + commentaire.setTexte(texte); + commentaire.setAuteur(auteur); + commentaire.setPhoto(photo); + commentaire.setDateCommentaire(LocalDateTime.now()); + return commentaire; + } +} + diff --git a/src/test/java/local/epul4a/fotosharing/integration/PhotoUploadIntegrationTest.java b/src/test/java/local/epul4a/fotosharing/integration/PhotoUploadIntegrationTest.java new file mode 100644 index 0000000..e1fa27b --- /dev/null +++ b/src/test/java/local/epul4a/fotosharing/integration/PhotoUploadIntegrationTest.java @@ -0,0 +1,288 @@ +package local.epul4a.fotosharing.integration; + +import local.epul4a.fotosharing.dto.PhotoDTO; +import local.epul4a.fotosharing.model.Photo; +import local.epul4a.fotosharing.model.Utilisateur; +import local.epul4a.fotosharing.repository.PhotoRepository; +import local.epul4a.fotosharing.repository.UtilisateurRepository; +import local.epul4a.fotosharing.service.PhotoService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests d'intégration pour le flux complet d'upload de photos + * Teste : Envoi fichier -> Vérification présence sur disque -> Vérification entrée en BDD + */ +@SpringBootTest +@ActiveProfiles("test") +class PhotoUploadIntegrationTest { + + @Autowired + private PhotoService photoService; + + @Autowired + private PhotoRepository photoRepository; + + @Autowired + private UtilisateurRepository utilisateurRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Value("${file.upload-dir}") + private String uploadDir; + + private Utilisateur testUser; + private Path uploadPath; + + @BeforeEach + void setUp() throws IOException { + // Créer le dossier d'upload pour les tests + uploadPath = Paths.get(uploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // Créer un utilisateur de test + testUser = new Utilisateur(); + testUser.setEmail("testupload@example.com"); + testUser.setNom("Upload"); + testUser.setPrenom("Test"); + testUser.setPassword(passwordEncoder.encode("password")); + testUser = utilisateurRepository.save(testUser); + } + + @AfterEach + void tearDown() throws IOException { + // Nettoyer les photos de test + photoRepository.findAll().forEach(photo -> { + try { + // Supprimer les fichiers sur disque + Path originalFile = uploadPath.resolve(photo.getUuidFichier()); + Path thumbFile = uploadPath.resolve(photo.getUuidThumbnail()); + Files.deleteIfExists(originalFile); + Files.deleteIfExists(thumbFile); + } catch (IOException e) { + // Ignorer les erreurs de suppression + } + }); + + // Supprimer de la BDD + photoRepository.deleteAll(); + utilisateurRepository.deleteAll(); + } + + @Test + void testCompleteUploadFlow_ShouldCreateFileOnDiskAndDatabaseEntry() throws IOException { + // Given - Créer un fichier JPEG valide + byte[] jpegContent = createValidJpegBytes(); + MockMultipartFile file = new MockMultipartFile( + "file", + "test-photo.jpg", + "image/jpeg", + jpegContent + ); + + // When - Upload de la photo + PhotoDTO uploadedPhoto = photoService.store(file, "PUBLIC", testUser.getEmail()); + + // Then - Vérifier que le DTO est retourné + assertNotNull(uploadedPhoto, "Le DTO de la photo uploadée ne doit pas être null"); + assertNotNull(uploadedPhoto.getId(), "L'ID de la photo doit être généré"); + assertEquals("test-photo.jpg", uploadedPhoto.getNomFichierOriginal()); + assertEquals("PUBLIC", uploadedPhoto.getVisibilite()); + assertEquals("image/jpeg", uploadedPhoto.getMimeType()); + assertEquals(jpegContent.length, uploadedPhoto.getTailleFichier()); + + // Then - Vérifier la présence du fichier ORIGINAL sur le disque + Path originalFile = uploadPath.resolve(uploadedPhoto.getUuidFichier()); + assertTrue(Files.exists(originalFile), + "Le fichier original doit exister sur le disque : " + originalFile); + assertEquals(jpegContent.length, Files.size(originalFile), + "La taille du fichier sur disque doit correspondre"); + + // Then - Vérifier la présence de la MINIATURE sur le disque + Path thumbnailFile = uploadPath.resolve(uploadedPhoto.getUuidThumbnail()); + assertTrue(Files.exists(thumbnailFile), + "La miniature doit exister sur le disque : " + thumbnailFile); + assertTrue(Files.size(thumbnailFile) > 0, + "La miniature ne doit pas être vide"); + + // Then - Vérifier l'entrée en BDD + Photo photoInDb = photoRepository.findById(uploadedPhoto.getId()).orElse(null); + assertNotNull(photoInDb, "La photo doit être enregistrée en BDD"); + assertEquals("test-photo.jpg", photoInDb.getNomFichierOriginal()); + assertEquals(Photo.Visibilite.PUBLIC, photoInDb.getVisibilite()); + assertEquals(testUser.getId(), photoInDb.getProprietaire().getId()); + assertEquals("image/jpeg", photoInDb.getMimeType()); + assertEquals((long) jpegContent.length, photoInDb.getTailleFichier()); + + // Then - Vérifier les métadonnées + assertNotNull(photoInDb.getLargeur(), "La largeur doit être extraite"); + assertNotNull(photoInDb.getHauteur(), "La hauteur doit être extraite"); + assertTrue(photoInDb.getLargeur() > 0, "La largeur doit être positive"); + assertTrue(photoInDb.getHauteur() > 0, "La hauteur doit être positive"); + } + + @Test + void testUploadPngFile_ShouldDetectCorrectMimeType() { + // Given - Créer un fichier PNG valide + byte[] pngContent = createValidPngBytes(); + MockMultipartFile file = new MockMultipartFile( + "file", + "test-image.png", + "image/png", + pngContent + ); + + // When + PhotoDTO uploadedPhoto = photoService.store(file, "PRIVATE", testUser.getEmail()); + + // Then - Vérifier le type MIME détecté + assertNotNull(uploadedPhoto); + assertEquals("image/png", uploadedPhoto.getMimeType(), + "Le type MIME doit être détecté comme PNG"); + + // Then - Vérifier en BDD + Photo photoInDb = photoRepository.findById(uploadedPhoto.getId()).orElse(null); + assertNotNull(photoInDb); + assertEquals("image/png", photoInDb.getMimeType()); + } + + @Test + void testUploadMultiplePhotos_ShouldCreateSeparateFiles() throws IOException { + // Given - Uploader plusieurs photos + for (int i = 1; i <= 3; i++) { + byte[] content = createValidJpegBytes(); + MockMultipartFile file = new MockMultipartFile( + "file", + "photo-" + i + ".jpg", + "image/jpeg", + content + ); + + // When + photoService.store(file, "PUBLIC", testUser.getEmail()); + } + + // Then - Vérifier que 3 photos sont en BDD + long photoCount = photoRepository.count(); + assertEquals(3, photoCount, "3 photos doivent être enregistrées"); + + // Then - Vérifier que tous les fichiers existent + photoRepository.findAll().forEach(photo -> { + Path originalFile = uploadPath.resolve(photo.getUuidFichier()); + Path thumbFile = uploadPath.resolve(photo.getUuidThumbnail()); + + assertTrue(Files.exists(originalFile), + "Le fichier original doit exister : " + photo.getUuidFichier()); + assertTrue(Files.exists(thumbFile), + "La miniature doit exister : " + photo.getUuidThumbnail()); + }); + } + + @Test + void testUploadWithDifferentVisibilities() { + // Test chaque type de visibilité + Photo.Visibilite[] visibilities = {Photo.Visibilite.PUBLIC, Photo.Visibilite.PRIVATE, Photo.Visibilite.SHARED}; + + for (Photo.Visibilite visibility : visibilities) { + // Given + byte[] content = createValidJpegBytes(); + MockMultipartFile file = new MockMultipartFile( + "file", + "photo-" + visibility.name().toLowerCase() + ".jpg", + "image/jpeg", + content + ); + + // When + PhotoDTO uploadedPhoto = photoService.store(file, visibility.name(), testUser.getEmail()); + + // Then + assertEquals(visibility.name(), uploadedPhoto.getVisibilite(), + "La visibilité doit être " + visibility); + + Photo photoInDb = photoRepository.findById(uploadedPhoto.getId()).orElse(null); + assertNotNull(photoInDb); + assertEquals(visibility, photoInDb.getVisibilite()); + } + } + + @Test + void testUpload_FileNameWithUUID_ShouldBeUnique() { + // Given - Uploader deux fichiers avec le même nom + byte[] content = createValidJpegBytes(); + + MockMultipartFile file1 = new MockMultipartFile("file", "same-name.jpg", "image/jpeg", content); + MockMultipartFile file2 = new MockMultipartFile("file", "same-name.jpg", "image/jpeg", content); + + // When + PhotoDTO photo1 = photoService.store(file1, "PUBLIC", testUser.getEmail()); + PhotoDTO photo2 = photoService.store(file2, "PUBLIC", testUser.getEmail()); + + // Then - Les UUID doivent être différents + assertNotEquals(photo1.getUuidFichier(), photo2.getUuidFichier(), + "Les UUID des fichiers doivent être différents même avec le même nom original"); + + // Then - Les deux fichiers doivent exister sur le disque + assertTrue(Files.exists(uploadPath.resolve(photo1.getUuidFichier()))); + assertTrue(Files.exists(uploadPath.resolve(photo2.getUuidFichier()))); + } + + // ===================== Méthodes Utilitaires ===================== + + /** + * Crée un tableau de bytes représentant un JPEG valide minimal + */ + private byte[] createValidJpegBytes() { + // Magic number JPEG + données minimales pour créer une image valide + return new byte[]{ + (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, // JPEG header + 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, // JFIF marker + 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + (byte) 0xFF, (byte) 0xDB, 0x00, 0x43, 0x00, // Quantization table + 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, + 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, + 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, 0x13, + 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, + 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, 0x22, + 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, 0x2C, + 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, + 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, 0x32, + (byte) 0xFF, (byte) 0xD9 // End of image + }; + } + + /** + * Crée un tableau de bytes représentant un PNG valide minimal + */ + private byte[] createValidPngBytes() { + // Magic number PNG + données minimales + return new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 pixel + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, (byte) 0xC4, (byte) 0x89, + 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, // IDAT chunk + 0x78, (byte) 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, // IEND chunk + (byte) 0xAE, 0x42, 0x60, (byte) 0x82 + }; + } +} + diff --git a/src/test/java/local/epul4a/fotosharing/mapper/PhotoMapperTest.java b/src/test/java/local/epul4a/fotosharing/mapper/PhotoMapperTest.java new file mode 100644 index 0000000..b67cc20 --- /dev/null +++ b/src/test/java/local/epul4a/fotosharing/mapper/PhotoMapperTest.java @@ -0,0 +1,153 @@ +package local.epul4a.fotosharing.mapper; + +import local.epul4a.fotosharing.dto.PhotoDTO; +import local.epul4a.fotosharing.dto.UtilisateurDTO; +import local.epul4a.fotosharing.model.Photo; +import local.epul4a.fotosharing.model.Utilisateur; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour PhotoMapper + * Teste la conversion bidirectionnelle entre Photo et PhotoDTO + */ +class PhotoMapperTest { + + @Test + void testToDTO_WithCompletePhoto_ShouldMapAllFields() { + // Given - Créer un utilisateur + Utilisateur utilisateur = new Utilisateur(); + utilisateur.setId(1L); + utilisateur.setEmail("test@example.com"); + utilisateur.setNom("Dupont"); + utilisateur.setPrenom("Jean"); + + // Given - Créer une photo complète + Photo photo = new Photo(); + photo.setId(100L); + photo.setNomFichierOriginal("vacances.jpg"); + photo.setUuidFichier("uuid-123-vacances.jpg"); + photo.setUuidThumbnail("thumb-uuid-123-vacances.jpg"); + photo.setDateUpload(LocalDateTime.of(2025, 12, 3, 10, 30)); + photo.setVisibilite(Photo.Visibilite.PUBLIC); + photo.setProprietaire(utilisateur); + photo.setMimeType("image/jpeg"); + photo.setTailleFichier(2457600L); + photo.setLargeur(1920); + photo.setHauteur(1080); + + // When - Convertir en DTO + PhotoDTO dto = PhotoMapper.toDTO(photo); + + // Then - Vérifier tous les champs + assertNotNull(dto, "Le DTO ne doit pas être null"); + assertEquals(100L, dto.getId()); + assertEquals("vacances.jpg", dto.getNomFichierOriginal()); + assertEquals("uuid-123-vacances.jpg", dto.getUuidFichier()); + assertEquals("thumb-uuid-123-vacances.jpg", dto.getUuidThumbnail()); + assertEquals(LocalDateTime.of(2025, 12, 3, 10, 30), dto.getDateUpload()); + assertEquals("PUBLIC", dto.getVisibilite()); + assertEquals("image/jpeg", dto.getMimeType()); + assertEquals(2457600L, dto.getTailleFichier()); + assertEquals(1920, dto.getLargeur()); + assertEquals(1080, dto.getHauteur()); + + // Then - Vérifier le propriétaire + assertNotNull(dto.getProprietaire(), "Le propriétaire ne doit pas être null"); + assertEquals(1L, dto.getProprietaire().getId()); + assertEquals("test@example.com", dto.getProprietaire().getEmail()); + assertEquals("Dupont", dto.getProprietaire().getNom()); + assertEquals("Jean", dto.getProprietaire().getPrenom()); + } + + @Test + void testToDTO_WithNullPhoto_ShouldReturnNull() { + // When - Convertir null + PhotoDTO dto = PhotoMapper.toDTO(null); + + // Then + assertNull(dto, "Le DTO doit être null quand la photo est null"); + } + + @Test + void testToDTO_WithPhotoWithoutOwner_ShouldNotFail() { + // Given - Photo sans propriétaire + Photo photo = new Photo(); + photo.setId(50L); + photo.setNomFichierOriginal("test.png"); + photo.setVisibilite(Photo.Visibilite.PRIVATE); + + // When + PhotoDTO dto = PhotoMapper.toDTO(photo); + + // Then + assertNotNull(dto); + assertEquals(50L, dto.getId()); + assertNull(dto.getProprietaire(), "Le propriétaire doit être null"); + } + + @Test + void testToDTO_WithMinimalPhoto_ShouldMapBasicFields() { + // Given - Photo avec champs minimaux + Photo photo = new Photo(); + photo.setId(25L); + photo.setNomFichierOriginal("minimal.jpg"); + photo.setVisibilite(Photo.Visibilite.SHARED); + + // When + PhotoDTO dto = PhotoMapper.toDTO(photo); + + // Then + assertNotNull(dto); + assertEquals(25L, dto.getId()); + assertEquals("minimal.jpg", dto.getNomFichierOriginal()); + assertEquals("SHARED", dto.getVisibilite()); + assertNull(dto.getMimeType()); + assertNull(dto.getTailleFichier()); + } + + @Test + void testToDTO_MetadataMapping() { + // Given - Photo avec métadonnées + Photo photo = new Photo(); + photo.setId(1L); + photo.setNomFichierOriginal("hd-photo.jpg"); + photo.setVisibilite(Photo.Visibilite.PUBLIC); + photo.setMimeType("image/png"); + photo.setTailleFichier(5242880L); // 5 MB + photo.setLargeur(3840); + photo.setHauteur(2160); + + // When + PhotoDTO dto = PhotoMapper.toDTO(photo); + + // Then - Vérifier les métadonnées + assertEquals("image/png", dto.getMimeType()); + assertEquals(5242880L, dto.getTailleFichier()); + assertEquals(3840, dto.getLargeur()); + assertEquals(2160, dto.getHauteur()); + } + + @Test + void testToDTO_AllVisibilityTypes() { + // Test pour chaque type de visibilité + for (Photo.Visibilite visibilite : Photo.Visibilite.values()) { + // Given + Photo photo = new Photo(); + photo.setId(1L); + photo.setNomFichierOriginal("test.jpg"); + photo.setVisibilite(visibilite); + + // When + PhotoDTO dto = PhotoMapper.toDTO(photo); + + // Then + assertEquals(visibilite.name(), dto.getVisibilite(), + "La visibilité " + visibilite + " doit être correctement mappée"); + } + } +} + diff --git a/src/test/java/local/epul4a/fotosharing/security/SecurityServiceTest.java b/src/test/java/local/epul4a/fotosharing/security/SecurityServiceTest.java new file mode 100644 index 0000000..5c1a82e --- /dev/null +++ b/src/test/java/local/epul4a/fotosharing/security/SecurityServiceTest.java @@ -0,0 +1,319 @@ +package local.epul4a.fotosharing.security; + +import local.epul4a.fotosharing.service.PartageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour SecurityService + * Teste les scénarios d'accès aux photos (READ / COMMENT / ADMIN) + */ +@ExtendWith(MockitoExtension.class) +class SecurityServiceTest { + + @Mock + private PartageService partageService; + + @Mock + private Authentication authentication; + + private SecurityService securityService; + + @BeforeEach + void setUp() { + securityService = new SecurityService(partageService); + } + + // ===================== TESTS canAccessPhoto ===================== + + @Test + void canAccessPhoto_UserAAccessesPrivatePhotoB_ShouldReturnFalse() { + // Given - User A essaie d'accéder à Photo B privée (pas de partage) + Long photoId = 100L; + String userAEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userAEmail); + when(partageService.canView(photoId, userAEmail)).thenReturn(false); + + // When + boolean canAccess = securityService.canAccessPhoto(authentication, photoId); + + // Then + assertFalse(canAccess, "User A ne devrait PAS pouvoir accéder à Photo B privée"); + verify(partageService).canView(photoId, userAEmail); + } + + @Test + void canAccessPhoto_UserAAccessesSharedPhotoB_ShouldReturnTrue() { + // Given - User A accède à Photo B partagée avec lui (READ) + Long photoId = 200L; + String userAEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userAEmail); + when(partageService.canView(photoId, userAEmail)).thenReturn(true); + + // When + boolean canAccess = securityService.canAccessPhoto(authentication, photoId); + + // Then + assertTrue(canAccess, "User A DEVRAIT pouvoir accéder à Photo B partagée"); + verify(partageService).canView(photoId, userAEmail); + } + + @Test + void canAccessPhoto_UserAAccessesOwnPhoto_ShouldReturnTrue() { + // Given - User A accède à sa propre photo + Long photoId = 300L; + String userAEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userAEmail); + when(partageService.canView(photoId, userAEmail)).thenReturn(true); // Propriétaire = accès + + // When + boolean canAccess = securityService.canAccessPhoto(authentication, photoId); + + // Then + assertTrue(canAccess, "User A DEVRAIT pouvoir accéder à sa propre photo"); + } + + @Test + void canAccessPhoto_UnauthenticatedUser_ShouldReturnFalse() { + // Given - Utilisateur non authentifié + when(authentication.isAuthenticated()).thenReturn(false); + + // When + boolean canAccess = securityService.canAccessPhoto(authentication, 100L); + + // Then + assertFalse(canAccess, "Un utilisateur non authentifié ne devrait PAS avoir accès"); + verify(partageService, never()).canView(anyLong(), anyString()); + } + + @Test + void canAccessPhoto_NullAuthentication_ShouldReturnFalse() { + // When + boolean canAccess = securityService.canAccessPhoto(null, 100L); + + // Then + assertFalse(canAccess, "Avec authentication null, l'accès devrait être refusé"); + verify(partageService, never()).canView(anyLong(), anyString()); + } + + @Test + void canAccessPhoto_PublicPhoto_ShouldReturnTrue() { + // Given - Photo publique (accessible par tous les utilisateurs authentifiés) + Long photoId = 400L; + String userEmail = "anyone@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canView(photoId, userEmail)).thenReturn(true); + + // When + boolean canAccess = securityService.canAccessPhoto(authentication, photoId); + + // Then + assertTrue(canAccess, "Une photo publique devrait être accessible"); + } + + // ===================== TESTS canCommentPhoto ===================== + + @Test + void canCommentPhoto_UserWithCommentRights_ShouldReturnTrue() { + // Given - User A a le droit COMMENT sur Photo B + Long photoId = 500L; + String userEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canComment(photoId, userEmail)).thenReturn(true); + + // When + boolean canComment = securityService.canCommentPhoto(authentication, photoId); + + // Then + assertTrue(canComment, "User A DEVRAIT pouvoir commenter Photo B"); + verify(partageService).canComment(photoId, userEmail); + } + + @Test + void canCommentPhoto_UserWithoutCommentRights_ShouldReturnFalse() { + // Given - User A n'a que READ (pas COMMENT) + Long photoId = 600L; + String userEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canComment(photoId, userEmail)).thenReturn(false); + + // When + boolean canComment = securityService.canCommentPhoto(authentication, photoId); + + // Then + assertFalse(canComment, "User A ne devrait PAS pouvoir commenter (pas de droit COMMENT)"); + } + + @Test + void canCommentPhoto_UnauthenticatedUser_ShouldReturnFalse() { + // Given + when(authentication.isAuthenticated()).thenReturn(false); + + // When + boolean canComment = securityService.canCommentPhoto(authentication, 100L); + + // Then + assertFalse(canComment); + verify(partageService, never()).canComment(anyLong(), anyString()); + } + + // ===================== TESTS canAdminPhoto ===================== + + @Test + void canAdminPhoto_OwnerOfPhoto_ShouldReturnTrue() { + // Given - Propriétaire de la photo + Long photoId = 700L; + String ownerEmail = "owner@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(ownerEmail); + when(partageService.canAdmin(photoId, ownerEmail)).thenReturn(true); + + // When + boolean canAdmin = securityService.canAdminPhoto(authentication, photoId); + + // Then + assertTrue(canAdmin, "Le propriétaire DEVRAIT avoir les droits ADMIN"); + } + + @Test + void canAdminPhoto_UserWithAdminRights_ShouldReturnTrue() { + // Given - User A a le droit ADMIN sur Photo B (partagé par le propriétaire) + Long photoId = 800L; + String userEmail = "admin@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canAdmin(photoId, userEmail)).thenReturn(true); + + // When + boolean canAdmin = securityService.canAdminPhoto(authentication, photoId); + + // Then + assertTrue(canAdmin); + } + + @Test + void canAdminPhoto_UserWithoutAdminRights_ShouldReturnFalse() { + // Given - User A n'a que READ ou COMMENT (pas ADMIN) + Long photoId = 900L; + String userEmail = "user@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canAdmin(photoId, userEmail)).thenReturn(false); + + // When + boolean canAdmin = securityService.canAdminPhoto(authentication, photoId); + + // Then + assertFalse(canAdmin, "User sans droit ADMIN ne devrait PAS pouvoir administrer"); + } + + @Test + void canAdminPhoto_NullAuthentication_ShouldReturnFalse() { + // When + boolean canAdmin = securityService.canAdminPhoto(null, 100L); + + // Then + assertFalse(canAdmin); + verify(partageService, never()).canAdmin(anyLong(), anyString()); + } + + // ===================== TESTS Albums ===================== + + @Test + void canViewAlbum_UserWithAccess_ShouldReturnTrue() { + // Given + Long albumId = 1L; + String userEmail = "user@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canViewAlbum(albumId, userEmail)).thenReturn(true); + + // When + boolean canView = securityService.canViewAlbum(authentication, albumId); + + // Then + assertTrue(canView); + verify(partageService).canViewAlbum(albumId, userEmail); + } + + @Test + void canCommentAlbum_UserWithoutAccess_ShouldReturnFalse() { + // Given + Long albumId = 2L; + String userEmail = "user@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userEmail); + when(partageService.canCommentAlbum(albumId, userEmail)).thenReturn(false); + + // When + boolean canComment = securityService.canCommentAlbum(authentication, albumId); + + // Then + assertFalse(canComment); + } + + @Test + void canAdminAlbum_UnauthenticatedUser_ShouldReturnFalse() { + // Given + when(authentication.isAuthenticated()).thenReturn(false); + + // When + boolean canAdmin = securityService.canAdminAlbum(authentication, 1L); + + // Then + assertFalse(canAdmin); + verify(partageService, never()).canAdminAlbum(anyLong(), anyString()); + } + + // ===================== TESTS Scénarios Complexes ===================== + + @Test + void scenarioComplete_UserATriesDifferentActionsOnPhotoB() { + // Scénario : User A a seulement READ sur Photo B + Long photoId = 1000L; + String userAEmail = "userA@example.com"; + + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(userAEmail); + when(partageService.canView(photoId, userAEmail)).thenReturn(true); + when(partageService.canComment(photoId, userAEmail)).thenReturn(false); + when(partageService.canAdmin(photoId, userAEmail)).thenReturn(false); + + // When/Then - User A peut voir + assertTrue(securityService.canAccessPhoto(authentication, photoId), + "User A devrait pouvoir VOIR la photo"); + + // When/Then - User A ne peut pas commenter + assertFalse(securityService.canCommentPhoto(authentication, photoId), + "User A ne devrait PAS pouvoir COMMENTER"); + + // When/Then - User A ne peut pas administrer + assertFalse(securityService.canAdminPhoto(authentication, photoId), + "User A ne devrait PAS pouvoir ADMINISTRER"); + } +} + diff --git a/target/FotoSharing-0.0.1-SNAPSHOT.war b/target/FotoSharing-0.0.1-SNAPSHOT.war index b5deac677cdf012ce66401193c117c029b2f268e..e8e548e516cd6e5ab955f5ec150c221bc5d58182 100644 GIT binary patch delta 9697 zcmZYF30zI-{|E4U&vU!d9_^dR5``!grIfV=X(1BHK3ZgpEXf+#XLJ!g8AZ6MY}XPB zV;z%yEGe=^DY8VC|Mzp^hxyOz^?9G~{XWlg?zzj{)0{K6kL|atJigyjC%})%3?%C6 z>XPcTg<<@1Y6$Dfs8kf~Rd1CPyEs;QAXYeYN2pFyxZD+5VjF%}u*Wv#uHcOAsk=gF zY~SA%x?;QPmC!=5=AK}xh%ON{(3w>t1YmoyMCgyL**zf;+n9TT54MNzHH>SP3VwJT zS}OQsyS-ExfbF|d!I!ow?=oSXRCtl5*ik98;1y4m%t)p4K-ehu{2vPTiW}v^bef=| z2RG1F**q4q#Nxse;jS7ww6;b-v9VgH6^H16$H=IIBX{5%pDUTBs^dH15krsG+4D;3 z>8k=i3Y{8ZtE zrvHMz4U(V!>8|i95$sj%meB4o%W4^0BNlln%vXH#sPN=3Yn4n#<+FxO6W=_V<*EAe zJKKnw|GhM{G*v8()nnGdE$pHA5j{?49^#x-c2My#3W+NJc|@^QQ4 z`1p~oS6}^ecv5-H#hodA-tBZU-+nf0!eOmH3tC??)(Ty3TW#u~c1_EBoaL$1BXNGK z=BC=LUL@V;u(W>BUzg+yd#%2a7{}o5~y_ z7C*_~Za3#zdzLtO)Jp%Sf6QvL(rDb4_8ZfKKe@>>&t1&!n%XMreE%UC)o&lBw(sv3 z>|g92om>#@w$ATbeq40puDOToW*uxhT-WR5o@G(_>7&8`LEfWW|pW@c9z|f@b zveDS_{I0^_TKT2z*`BBES8jjd6dsiQ+QDRZT<)0{bq9_5wcW4yTyp9|;Ig93ht9=^ z=i1vpUEp#ud*GRfr@KBFAK8(4yv3QY=nuU$bRIi~bdHX@ym!FjuqQJoXWF$Gtmi$o z%w+A;*6EMV+LWAl{kr-{81!iP9596Pd8@A?)!92>6~v!z?^kzpBvO>Az? zm%|>Jxrz2>36=4SR`ZlvcGg?}{>8QHx}`6vczicrw|er8k+ogQd=BhRkcY7|cF)FX zR4hJz^!5GcuAfb|jX1h@-^62iFANI~)qAJAy|sBc#joFW(|Z{MDxO?hxPDvs@9Bfx z605_j3Sz2~9~yUDx96(!f#*Z2!@J!pUwhX3#Ge6=j_;asr2gF4R+WG3Fm_HHJ0dZB znVDzYu$cbto!b^wSamP6aaw4eWIR(h%f92(yd{>M7Y$jTeycdCsNOZL=yoI5dC9f= z=TASEAx-PL$@_ZFjSX+k8YZ6hTyB=%=3(uggtCA`i7g{N7C4mDT5C4*Y}ERG(>eo> z-6!Y1@p)U9xTijGYkl#3voFtUKD0`RDYsfO+P$_-p8q556~_i|?!R}ZuA}vWg52wA zuNSpHHz;;N^ryrn2CY&Yhoy}BGXH#;+x!ik2fUA3^)W5}(~VezXVOQkhhsO}P6lrr z^qg`RK3TFrBPpX*qUC-EgZ<{}!)|){_4JxuV=HlbmnQesQR}{>O}3Whw4mY%tt8h| zLkF>W=IRAGY{=K9bz9{2MaOkZPKI2Ku%G%~+yD5@aO_u z50i6^dL=VfeL2V0iaVoIhr@rC*3b-nl~FO9@}H$6KVj*^Ys^bLc=04s{6xtb^ZuW) zWcE$g;{Trr0>l#m|1r@@CB4Ijil^4$Wh_U0>2!Q<4O0{uWlUW)#GX+yE4w57D8*iq~$?I|579Vrf!PL$3RM~V}r3&ok@LUE;ZrF5gXQQRrrDIOG0 zN)L(`#hcQT(u>lY(udNQ(vRXp@um1t{3-n@11JM20hB?MKuQoLm=Z!6Oc_E6r3|GE zql8h0Q^F}DC?hEmlu?vO%4o_M%2>)c%6Q5I%0$XFF-vng{Zb1CyEiIn-2B+3HHLdqh_V#*TAQc5yq8D%*og|dROlCp}j znzDwnma>lWJLM0`dP*uqLD@jrNZCY5qim*Zp`=qdC4;h+@+V~*C6kgx*-qI($)@b2 z?4sxZKQ%+C{DMgf%lv9+` zl)orvC}%0>DCa2`C>JSzQ!Y{dp%hatQ?5|1Qm#?1Q*KahQvRjfqTHt3q1>gEQ0`Gm zDP@%Vln0b@%0tQ{%45nC%2UcSN(JRP^_m=0!ucrX*p0tsL?m;>g5c_0zY2T5Q7SO^w@#b60o3X;JxupFd-6<{S;1y+MK zU@ce&eg}Vm^&k}}zy`1pYyxRuGuQ&s0S6giEBF&^1DPNTYzI3)HrNSvf!#m}a=;#t z3-Uld*bDZ7{onvN2vpz@I1G+}qo4pB1INJ$PzZ{^NpK3B27iGw;4C-?&Vvi!BKR9z z0{?(wa2Z?ySHU%K9ozsn!N1@ZxDD=ryPyQz1Eru0+y@UpId}*jfydwpcnY3@3h*4f z0F|H$yad&t2D}2VK`nR#-hy}FJ*Wd8z(-IIK7r5R3-~IfpFe>LDG&eyGN1Qpt zeL+9q1AKuW@CW_D05A{)fI%P-1c6`>0tSO2AQTJ*!$24q4#L3*FcL(7Q6LhG24lcj zFb<3d6Tn398<+&5Ks1N}v0yTY15?0MFbzxxGeA6;31)!=FdNJPbHO~22R5rQostZ608EN!5Xj@tOLJ;KfrpB3KU=i*a$X(G_VYy>u0GdDxXagOf3-o|KXabr717HY@fH5!uroarC z0}EgYtUxnh4Vr@%zy`Dgtw3wg2H1kOpdGLS_MkoJ06GE(&r z8*l^epgZsYo}dTt0^Xn}=mmO%KAr{ez=eQ*Jm_$WP_bx7ud~C z=nYjepCEcaPvHF=CBDVSbCv8IJ{O)5UnDi=8F@<998*qFv7i&){J!N%=7eG{74G6^ zm>;Tc%&*bI4l?n^lwPMasR_4ysARU{c-&rnV{V;wTq@(EsoB0^*5#PCQX_8t>=?E8 zP+Po}#q&po3&#AiMvWljYLAqxSHm;(>uS!&(?c&j^DZhPQRqBYGVx3G%d*AP?Sy7wdw?WQ6YLl!T;6)UO0@^rdMB~FR|Uc97YCgx6{yGip< zJfk83#h6#$hDxarheo_svWXZvPen8eSGr3y9>v!0g~pq2L$j!ujiGl`h)){*_FM6Y z3UO%EJ0*+P!L`^QS84eZYDQpTzxQ;5ODN0YYdr-i|z;^4dNhC!}b zM>ojOzejT|1fM+aMV(IKRa;AIJVPhPa!I}*kwmLWB)xySAC{T)b)PsnNSt<4uU5#Q zDaCpcNw;67*{H;6&ipy;8Mop17b+|SyJO-pxGJ0vGkrd1tU{{Z61QRTq{?KQos*bE z(pN(wasOp+zJ=l{U+l+^t_fQR%}$H^w{N%{jO)af7(5~l+MoY<5Z8)rFxZg_3&G^? zAA^nfeCk^Wco~g|A7qs{ zW!=;NPEqH5>3XuM_@O-dXZKz|DIEoUeuIBGk{&M_*8R8w?If;PH!a8UZ(7lpjz931 z6}nabU7@3(Yt}|Qu~{Hp#18+eWR1jK=-u?W9@$7DIjkd*c>S`Au@zEwI#FyfpZ zd(r#{wDn~}?pP`w+_r4a2QtjxJ)2*lq8Z;nx(KEP;=^GqvvYHs>#M7<^kaiH59L zy1Kh~jE0Kt>h9*eK%DO{_vAXMhIDfILXSOILTf{wq@F|n;+wWMq#xl}Iqg-ph-PqI z!j^xeK@EP1+E&8E3SLFU0O@D$-YAFpORM=Z+6GBC^Lw=ID^p8tC*^C_x080@gK3?u zpjpr7QxPoFXk{nmqlV6tHsvp<;UZf~FaKMkE4@;8Sxmcu-y18a;Vk>QGDymo6K!}E zHEd;%lS9AHu-C|8ZbGs@pGZZ3OzTL3lsuzmCWwz;)cDO^5QRxrQhGn-dfnlM?QwO!=+Do5p9Eo zr-XIcN7-VZZ}YED~m}f zYB*vuUP_IwvYUULr1gX?@2QI!rgusk8U}pR{WALMucEf2?1=9D@9o>va24pw7N?iP z%y^-7g_Qo76TdctsADB9;*)6`!m`I#ay@#HX54>#rL>7SM~4gg)REn!Ll%#_mH2Mx z@OIUorFxp*|Aop-%H=UMF<92DL=eOe%!HRx!%;T-v~feDDL183@7ufSGTK=%_-TD1 zAJv53enD#yKR|`A;P^Y2HpQoAkmdh%k6uX+$JwB?-*@MeTb zBL&`rzIz5+K1K>Ae7V>)7RG$*@(a}Pmaga8hV=cEt=#@n;QI86P5De}xXHYl)lkET z-W2}as)mlWJJmY8mIlqFuelCA8p7r$n{qvIj%~6jooO28WKqXVnAM#ZQ4u1u8{d|F z``)&vZ;y+t?W2wj4Ff)h8m_W;au3Fnhz`7(K3WZ4Lv1ViY0)*IuaGQI9>f}63fIsK z!tmH&CgWe#f|wTHLQ`yIPFcZ>pU4bmcKi(*f{B7#ndUG*=`H>nZ3js=@B-TU3mS*G zh8c?C=Xod<9UK0Biqjs%jO2QfDz6T~d+XZlT?$Qwiks=7#v3Id6SSbR61;v6TmRLZev7sP##a=eZUcEPS_$wbVwWVeO0=qT$x(+hjUE;|us!a?*6 zGv^>?b(Aj8^`fjHqoT;OWna9PXeSq=&(2`kSj4D%L*ERwz>5r3Ukxi;$hI%_!p!hz zOA)Ur+1uB?MXiN}+Shud=q#@n@*KTyBktI7-r+dW*-$A|3^B!)VL@-Pn=fp@)My75 z7_TpEJ0{EsH~Q|sRyyx&rIw+_Z1GY(6t{U|iCXMhApEr4FN!vX7E45&;1^BHn1k8@+Hb6s;01{&%=K4=Jn+(-K6h|Gg`AS;}s2c~YF#ihR!9mniPBHRL6U z*6PeEDIzpxMb=oC4y9*L85h5?@$S4q_iy-iHu5#^Wvz+NYua@8a{Z9!q zw3UBf(!isXvx()-58BO+CXJ+R<_44&E>ND@iX^#t8en$qTU!41Ly5{E1rS z)*Cf-h2eSgoEnqtt-|7diZ9bd=P=_~!sV^&YGsbP8ryj6xtlwhmoD}EW88T!E6-)k z9_?>(A+p>q-&X+{Wu`w`r7_&GUL8|s%gX`IQ}vm8|Nh;bv)^wm@EpEi)$6zzBp$Mw#BY&q^we$6!#>TYfy zaU}nzpiWn;trxHTBfqXye&A2n?`$hDH_1vlJK;&_=uO_?JDyi~(PQM@GMyUbRC?Yg zByjD!n91v2*>7(&IVtPahTuLSnO7RltiQjXZc)?cSr3kl7~8VS!J2-zkHt5#iLXAs z!iyVj0oz9fG)`!hQ+C_za@ALy9k8Zlab@Ke#Yh;kAkS=EqUgH zldGG&dN`($eVNDN!RZ|ft1WUbRomU9*P{ZP*YU+Cd+l;~I4maY@&0AMRyv3 zl$rm=Lgy9XTe2#p)X5H+80Cxo+{XQrh5Bhccp-nHLfdPTg5M$=|1T{Ji@Wyq&Y-%f77exLw~K zcdS1bnm?V?D|*m9jfwl+k+v~ICyY5f=hb3uw3STO9I{?xccsVcFsoA+B2HY}+-aoS z`#O&b_MFPI4L$nch>cA>%k`V)7X*zN?vm)V=)hU`^cq{=Et$GmxG(F{b4Xi@(A7o$ zQ;rQSY_`biNNnA|XLweO>sCH z8Sr{;@u>%Y#46FP%H~cJh=K|yIZGbioe{tR1Qqv zJxxB$?|jW9JuTSZ)a<^Gym3+(8AV$d8GZYOgi8r8t-6R5VPdGYNnBAcI?7hjm6tDd zOy+*Tby`k@wuXUw#X0p7fhpdGD1$g@%y~%Odj}91TnV5=+&W!u7d@0RwN;EnNB3^=5|E z86xh-m4^OTM3_2Dy@E&ls}~*y`|HA0J%k3C;*jzGyYhC_d1s2ovfpR^66usHx~tc1 z9E@IF6!1h`P%o&sZnu!Fi}j+6VfZugN$umYsC{;Zy#*ppUD@=VFz3oS?TQ4VMT`*> z#1t_@%n=L360t%`A=ZcuQW~*E?2s~uJyI5NKpYV##2G1vxFD`bdBhEIM?8=UNJXR) zQW>d&R7I*Go=A1122vBLh15psAa#*?NPVOM(hzBcG)9^rUPx1<8PXhSfwV+gA+3=% zNL$1k@j==l?U4>hN2C+d8SzE@5Pu{9>4F3zK}ayt6$wE?k#0zLqzBRy2}61zy^(OF z57HOuhxA7xkO9a*WDqhK`3V_<3`K?^!;znn5y(hn6fzo#M52%}$XMhTWE?Uc`4yRf zOhhIjlaVP%G%^*LhQuJT$aEwQnSsniW+AhYImldO9x@+UfW#vUkwwU2WC^kqS%xe} zRv;^pRfrBrKvpAbkhREfNFtJiNF*6aLDnJbkqyX3WD~L(*@A3EwjtXQJ(7y-Kz1U# zkln~0zmY z9=U*AL^6;|$lu66$YtaTauvCTTt{voH<3&v3&}=ukXy)YBp11Z+(qso_mMm#A9;X0 zL>?iJktfJgt^+^NLkTfEVNfY8lnv!OuIcY&!l2)WOX+zo) zZ{kDRk@lnm=}0<}&cv7a5q}aux{yE;M1o0I5<)^rH`1N-AU#PK=|y^zaMFkLCH+W$ z5&XVPk!&KH$riGeY$Mx=o}`i;WGC50c9T8ice0oKLG}>?*-s9T zgX9o7O#UQCNE%5eN69gAoSYyh$tiN0oFRXav*a8(PcD#)B!gTcf0KX6WpagFCD+Jx za)aC?nIwy3lN@r3+$Oo?4!KM2k^3Z%vq!y`7>X5pm9;r_n zkcOlYX-t|BFVd7WBh5(*(vq|ytw|fwmUt5%(vGwz9Y{ygiF78u#E|9a&E{kd0&$*-W;Otz;Y7 zPV^*|>>xYIF0z~KA-|Kolh9IY~~D)8q{K zi<~9r$a!*sTqGIf68W3_LoSmmnS@|Zj!Psua#oD`5k@`Ai1ugGiihP);3$b0gE6p@eQ6Zw~XCdCRae*%SuC`1q~ zF(xL&l$a57VnHm46)8omi47@DY>6EyL+nXe;y@gU6LBWxhzoHg<%t_{Cmy5%sYoi3 z%A^XZN~#f0Qk~QwHAyW}o75q7Nj*}ZG$0L0Bhr{OAzq{@X-1lp7NjLr6ns}ze8r_7)3^tND@WHkg?ckm24y1McS@jdeJfz zXElvyb%5ac?Yc`Z&hWvE&ONHUqfF@9NU_!J*ko%iyX@8rQ^hYKtp|#@`uwS6-qnjn zEK1+QouX7ta|ILcVFE*h+<|4iC>jj_j$Y>N1EN-Fb%c51DUJPWBf}$6TPbkzm{qpr9oIQt7^ilVg zw#C*_Mjf@m;WU1r7d~3NM7Rt(xJu(Bm!qTYmyY7LVZuokpd%blTx0;A?u4*NMiI>7 z<=3M9BfSV^&uSDs)gct>fr*6b{Wp!PiQ{BNX*DC!kPA zUZa>`%?CT*Zneru@Aw14x-1l~O29_h^^;!IO2W-lV;%0dI>@%~^}>oX0#UdsgSLD( zLnfln(S!S}-|(){ZpFxGl(CUfhwmO7ZJ54q=XWb~xFu_>-m5LIF1uFxm6D7l8KsSk zn*XpYbI*U4S<0sSN)DwMhv%x?-T&PT^~P>xb)WjVm9~z+o#alqlic!$eQFN>cZI1g zDP5SzOSmg+r7rI1v+{mtEN*IUWYqSD#n;llEw0EDatn)GmH6XocLViyae4N4!R=vJ zW%Zeoe%>vvr0#o2#^I!+I7e4ajEvg-ulbINCR^QM4i`)13Yj9L}Pd$D(ved`wx|-^p z?PlEHB;59PRidAM*I!15yWsfr=4{1*CtcJFK6{lP)QM-Fe>c%it{vM|SSI#Q#Wl2J zLgKJg{_x&@nW39jBwTch-U<7}+*I|wCqC!+h7aGyE6CVmP8t)rBxF4HJ!&uhTrV1}H`_Q*O zCmC&*DlB9&M%*;EG7W8SQ502ytL$Yx<5baBJZjQH-HD9HW-HNu+m}t9=k^vhGS{5z zKV!sAYiZW<>#%Do4stU)l?PSi-za)1FILK0rg+23 zwTChmMWE(qnSnMy(?`0RrHW9Exr{>FNg2CR9zYSMoroKHUrvE+^|6M=LAJp$c!~*? zYs#)xI5Bb>w!Osb0d-2Y`8JH@p9AY?z8tTo$f_PxGUjfPDw=3VE)3Pk@{2+>)nqI> zJVnUdu#yRB*lwY`@t2=a1dB-rCYKE4Vy`B0xFudo<0(_nhA8cN$zU8pSs9$ID8|wT zbsOdP?P_Nik??s|$=WZ&2Qb`9GgQ7q+egz)b}N-Cx+(AUa+_KdMkZO~1c(ieNhQmE z#FD(lb*}%|>t@USmmT`_hq_KUdar zC1#Yh;S?$s4C1s?=^Y2$<+fp(Bd>n|Nn_=2>?b?VDEHP5HDu($qtvD+}cGqzdPbJMb$r;}jC1r!`jiS3UYn9xM!e7%t{)@J&##jcG!`DoiV33Jw@z)vo z1Vvr`A?fOZOPF?T=tv=LCwM9LG8i4E>Ri=v^efK~^>~e(hN7RwP3E8- ztW7K4DiWi%iH0T9;@tSjT9>{3>j^%Lj!JyrcTl)$g5+nkl|A_S)kY{w=i=ZL{GPG< U;b8adls@j5W-=LnbsHJ|AA(EojQ{`u diff --git a/target/FotoSharing-0.0.1-SNAPSHOT.war.original b/target/FotoSharing-0.0.1-SNAPSHOT.war.original index ae3b6ab84406db1880094f10f2d37c4075c98dd1..9deb4b6ccd745800883f7bbfce4c374936cc74c2 100644 GIT binary patch delta 8202 zcmZA62|QJKAII@~@45C%gsh>Yl~P(zp~aMw#2x;g#kq~0o=txzb|C&uh=VH4M9sS4+w)q(MbN@p&ul`XkUP&VVTP3d98`*2Iu z|Kc5NYQ+0UR`E4e`n;lnst1?pssJwMRe@aWBtI^rB>7yX)RjlNYJV=l>TX<8)LpP- zyiw0qs!G;nOw!Kf8thO48-R1Ii<_*@!Yg(fy1Mb&twOO0%`L@0FxYp_9TAgcfpHuR6w*4o_Yg}lE zycUNYm!D_X;I{HwJfw*!ueX_;BtK8)%uOad4qY*(2G@%zwIr^ucGQ$tqdhV5m{J3{AqvYd&?56S>lXFAf4?59UWmJM6fXTH_rM=Y(#RE-z z+^J5{tNZk_>12F%deop()qU32IHTRqZ?6At+ivZRYb9tMT)oiIp;7w!E!v|rqeo8; z%5>aW-fil_T?Ji&?A|EfZ0cWRw`bzgmT^<=-76}pm^kiA^t5kNEAKo~l(qP5y})?x zS%<+sW`i{;1K$`Vg-296PoMHB@MfJkcMpzQH-14o_hX?}(F;bqdTB>Wb#ua2jB;=B zFmLhjU6EqMK=NTl0E9y+R zJIQ(NBiFEbPt#Kx{eHtmiVhyM%>UV{sr8oC7{0+}P13KQTWcrhUD)oC;56o3K*+{d z?+X)L0=oX{f2B?M!mRMtE4$uEj||uINIl$W>cIy6tF=3^chQ*iq#-Nx`%iNo^Y`F2 z#pVx3>P2Nf^;;_j7~Pxo`g~%*fh~tKC--QaAevfwH@z~ZalgG`X**_IpEN2ju4Z&k zm+`GTXPH};U#&50gt(t*cB9}@%69Kl&dX9tnh)r;{f(>TuE^9gCFKWebZ)Re@ymmg z6@iO#lM7p3IWoi9`PuANC$@J#6ZULpg~icr$=S8f^b4=>)i-+L7St>}^6I{B^ZGrV z5|P}fcCc~#36Ct}p4Clyd?qcTt^V1tYi_lcY=0KraoT{8=5vM(Z0cKC+s93B>Cg?H zsYm;6R5^}I-ShQ`S8{5!v-PwWljp{YC2ys(?M?D8#I5v9nqTzf{^V+}B5nw#=cxwXwhWl-s>$76otB0%DiDt zFIH`{Xc;{$GfB^P+WeZ$=7y|Jx_f0- zZl(LW+lMD;JMB@A!_jpHd(C!z z@Ycb=-dm?`zG=Cc*RB&Y-ub;RkKS7uy{Ym_zV+AQvI?hZV+-u&4{7tZUYh@7!==Z9 z*9Yv|QO(U^c2?@mb#Land^4vdZhe;w{~B}dQR|pLnsxg)X8EUelRw`Y zXZBqASn_f6tl!k^ovU&4)HzS*&(@!{(J8v-epj>oHoARpx9i%W-So2hism2IX?HQw zYCFH)c1_KwUROpsDQ+h8?kUZ*(aqW;g?uwD-=OWBn_ca}iJXuA0D z)emKh-rdfguVc1ghoARXbvWoe$iZtWqcjX(<%G_-i1y)RJ@|)(wl0W6^dX_p!@{^_HH;v_+p{HJ=v5B%td|HW)NcmzQnPi-K zJy^b_;fKZYEeFd-$PZ=zc^9AJ&_$(k1K+q0jn-4H=b1x#hRS1nt38xwr1a!Hhtwv@ z3PQms z1ocLJP+!y!^+y9xC>n^u&>%Dz4M9WEFf<&EKqJv8^cxzD#-MOC7L7v@C=!iF6HpYI zh$f-QXbPH&rlINRcQgadMA0Y)%|f%$95ffrL-WxBv=A*qi_sFa6fHx`Q7l@4;?PPI zk5-}8C;=s+KhPSq7Og|;(FT-+1lou;q0MLuN=92z3fhLYqaA1`+J$zbJ!mgVMQJD< z?L!%8KRSR8qC@B~I)aX(Oq7L=p=@*<<)B=20-Z#s&}noAoke-*96FCKpo{1dx{R)% ztLPfKj&7iv=oY$-?x4Hq9{LmAM-R|n=plN9^3mU@02QLg=m~m?o}uTc2o<9e^a8y^ zrRWtZL$A>r^cKBC@6iYJ5tX9~^a)j>&*%&KioPwu?|v*IDk2dz(IVPJhv*VLqE8Hn zh8PkfQjHiB6H=X+5;I~>Y7h%zNvw!9kza-_r<%l$*b@g*i_|8Lqz-W+bxA!^pEMv1 zNh9J+Tu5Wmgt(HXq#1D|%}EQ=lC&c3#DjQ})}#$-OT36T@geO_d(w__nMS6Q-^mOzlSGpkGK|Km86k$vX5kt{p0{SNDh(1Cu7Mt5l2npL(#bxO zLH3gaFt^Y^YQZ5BgAa!<7@Bi%du$-J;Sh=87Y;`-w3n~J;K!l!69XxT zgTN5RLHX1`8o(h8!(a}lF!bSI|E%hoSs40p=OKpv9NIiLkU}}E$IzEUIfjY&k6baX z$Uq9@a1uig4(7#G!w3w$xRZq;L_Sqgb@3<+-MMoVLl6g(7gfVZ4D!Xf7<%%Q*-HZ{ zlEVTF@{@kW5XGIS(yE~tgM4bxD+6g9PhI~pbSSI3{U8SU8kg5qLo$Xbyx8uIfi#mt z5{79Utlw6hT7^MAW%kZM8pTu57{WP}Vi>_86Hdj)A+IrTJfcMCc+D!gJ^g@vV!MZUi^Mu zu~t} zVWaL(%if8PWX9v;oK8PIY@__|7sq=)uoO0Uwest0fWcPfS@_R69TAN!er_(5pLy=v z)A&K9&Ek(fH4)BVtL}=B&wMW78IgknNBL_SekCg@7f*gmTcPOi)05vV#ihRAA3(nJ z-%oiIigrI;`sp<;brak0k&mr>X~?d{pX=dKJK{s3pTDH^5AsVJ^TFcZm*$DrXsc>l z5x*(aMU;LskaWaSY&r4^N|(>2eEz?4Cc+HYS@DV{7;II&R3aBc8?gvl+VjVioDf;y zfsepO;s0^@`9-`DIk+kqcW&3!Et&zGbH;X}71Et!Nj1qTGG^!;Q;ZP$Ap`B#wVVaoYZdymw*V0H0r9=Lv ziS7JNT|@)<>6c^EPG!1P?8V?GiB^{Xd7*m378kXWj`>+8cJi~d7ZY*VQ2A19#2TV< zC>EPA*b7S?ye`S7i%r$N<8bVaSCxu2Sb7`R?E8LVLR>RbaUWl>M)>IBd6lcgK&*k% z0$;a86JNK+q5y{tRpa*yBR$@_Anik*F1KA@hkH|Ar;Ks)7gA+QFi_l8C&g$KZC z6TZL~e~z#EW{5Csv{Gl>TcH$Qz(wTASKKLz`@Ufz4DpA)joL49<&W1Fj*V98%{%_t z2*79}Tuu4R`SJ@c!X?(~?U}fQFW6Gp<78_|Yi+zp$7C*&*TmzOsG%8eS%Dop@(Oz9!{IhWx8=mUFFCO{vQ*gp})Klf&M-0NyT|IZ_ zxgTHWS!{TzFL+%3@nB;y>Q#N0Th`!@Ap1%FkE{D*{PFXd$CIkwSz;@Oo~n9hMLC8p zYP+jneq5e)rPx?wu?gP_{HSo;PTh2bxk_XWH&-MfAiFpjvcNY{U?%T7E0Zn!jHgwUXrL zFcbDR8cAPt)=E-Ut9oZxi7V`+kfE7BUGZ&5rpiz}#JPsjU*k^x=UfNra_Oo69P5eM zc+6Ikqw}?@-K+BPz8AOUV{>7TKiTfm{5ys6aV;?&$6nI5qVLC6qDxUBekZ^G`G1dd zcIp}Um#Z+w?|cnmhtW!S@_j)B*2GEWK@o|y4}RA!U=3Cmj`^q(V}ZTsXouIU+%Lvq z?WwN$xI!hi7gpdzAy23#*B907@s6Q#uoeR_1SpS*U0A*N@3#^RwyMZNu^U5U(aHfo M9Bn=PyHTO|AG~d-=Kufz delta 7996 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