From a66b51e1e507d0df30b7da88171eb4b78de37ded Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 5 Jul 2020 18:57:19 +0200 Subject: [PATCH] Add unit tests --- .../net/friedl/fling/FlingConfiguration.java | 2 +- .../fling/controller/ArtifactController.java | 28 +- .../controller/CommonExceptionHandler.java | 45 +++ .../fling/controller/FlingController.java | 5 +- .../friedl/fling/model/dto/ArtifactDto.java | 2 +- .../repositories/ArtifactRepository.java | 2 +- .../friedl/fling/service/ArtifactService.java | 18 +- .../friedl/fling/service/FlingService.java | 7 +- .../fling/service/ServiceException.java | 42 --- .../service/archive/ArchiveException.java | 73 ----- .../fling/service/archive/ArchiveService.java | 14 +- .../archive/impl/FileSystemArchive.java | 31 +- .../controller/ArtifactControllerTest.java | 156 +++++++-- .../fling/controller/FlingControllerTest.java | 206 ++++++++++++ .../fling/service/ArtifactServiceTest.java | 137 ++++++++ .../fling/service/FlingServiceTest.java | 181 +++++++++++ .../archive/FileSystemArchiveTest.java | 300 ++++++++++++++++++ .../resources/filesystem/archive_path/.keep | 0 .../resources/filesystem/artifacts/artifact1 | 1 + .../resources/filesystem/artifacts/artifact2 | 1 + 20 files changed, 1045 insertions(+), 206 deletions(-) create mode 100644 service/fling/src/main/java/net/friedl/fling/controller/CommonExceptionHandler.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/service/ServiceException.java delete mode 100644 service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveException.java create mode 100644 service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java create mode 100644 service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java create mode 100644 service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java create mode 100644 service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java create mode 100644 service/fling/src/test/resources/filesystem/archive_path/.keep create mode 100644 service/fling/src/test/resources/filesystem/artifacts/artifact1 create mode 100644 service/fling/src/test/resources/filesystem/artifacts/artifact2 diff --git a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java index b710f87..7f97420 100644 --- a/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java +++ b/service/fling/src/main/java/net/friedl/fling/FlingConfiguration.java @@ -20,7 +20,7 @@ public class FlingConfiguration { public MessageDigest keyHashDigest() throws NoSuchAlgorithmException { return MessageDigest.getInstance("SHA-512"); } - + @Bean public ObjectMapper objectMapper() { SimpleModule simpleModule = new SimpleModule(); diff --git a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java index add9f4c..15f1c4d 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/ArtifactController.java @@ -1,7 +1,6 @@ package net.friedl.fling.controller; import java.io.IOException; -import java.io.UncheckedIOException; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; @@ -16,18 +15,15 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.service.ArtifactService; import net.friedl.fling.service.archive.ArchiveService; -@Slf4j @RestController @RequestMapping("/api/artifacts") @Tag(name = "artifact", description = "Operations on /api/artifacts") @@ -42,36 +38,28 @@ public class ArtifactController { this.archiveService = archiveService; } + @ApiResponse(responseCode = "404", description = "No artifact with `id` found") @GetMapping(path = "/{id}") public ArtifactDto getArtifact(@PathVariable UUID id) { return artifactService.getById(id); } + @ApiResponse(responseCode = "404", description = "No artifact with `id` found") @DeleteMapping(path = "/{id}") - public void deleteArtifact(@PathVariable UUID id) { + public void deleteArtifact(@PathVariable UUID id) throws IOException { artifactService.delete(id); } - @Operation(requestBody = @RequestBody( - content = @Content(schema = @Schema(type = "string", format = "binary")))) + @RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary"))) @PostMapping(path = "/{id}/data") - public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) { - try { + public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) throws IOException { archiveService.storeArtifact(id, request.getInputStream()); - } catch (IOException e) { - log.error("Could not read input from stream", e); - throw new UncheckedIOException(e); - } } - @Operation(responses = { - @ApiResponse(responseCode = "200", - content = @Content( - mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, - schema = @Schema(type = "string", format = "binary"))) - }) + @ApiResponse(responseCode = "200", + content = @Content(schema = @Schema(type = "string", format = "binary"))) @GetMapping(path = "/{id}/data") - public ResponseEntity downloadArtifact(@PathVariable UUID id) { + public ResponseEntity downloadArtifact(@PathVariable UUID id) throws IOException { ArtifactDto artifactDto = artifactService.getById(id); InputStreamResource data = new InputStreamResource(archiveService.getArtifact(id)); diff --git a/service/fling/src/main/java/net/friedl/fling/controller/CommonExceptionHandler.java b/service/fling/src/main/java/net/friedl/fling/controller/CommonExceptionHandler.java new file mode 100644 index 0000000..f149e3e --- /dev/null +++ b/service/fling/src/main/java/net/friedl/fling/controller/CommonExceptionHandler.java @@ -0,0 +1,45 @@ +package net.friedl.fling.controller; + +import java.io.IOException; +import java.io.UncheckedIOException; +import javax.persistence.EntityNotFoundException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ControllerAdvice +public class CommonExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({EntityNotFoundException.class, IOException.class, UncheckedIOException.class}) + public ResponseEntity handleNotFound(Exception ex, WebRequest request) + throws Exception { + + HttpHeaders headers = new HttpHeaders(); + + if (ex instanceof IOException) { + log.error("IO Error", ex); + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return handleExceptionInternal(ex, null, headers, status, request); + } + + if (ex instanceof UncheckedIOException) { + log.error("IO Error", ex); + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return handleExceptionInternal(ex, null, headers, status, request); + } + + if (ex instanceof EntityNotFoundException) { + log.error("Entity not found", ex); + HttpStatus status = HttpStatus.NOT_FOUND; + return handleExceptionInternal(ex, null, headers, status, request); + } + + return super.handleException(ex, request); + } +} diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index 3e9c0bf..bdd5163 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -1,5 +1,6 @@ package net.friedl.fling.controller; +import java.io.IOException; import java.util.List; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; @@ -70,7 +71,7 @@ public class FlingController { } @DeleteMapping("/{id}") - public void deleteFling(@PathVariable UUID id) { + public void deleteFling(@PathVariable UUID id) throws IOException { flingService.delete(id); } @@ -81,7 +82,7 @@ public class FlingController { schema = @Schema(type = "string", format = "binary"))) }) @GetMapping(path = "/{id}/data") - public ResponseEntity getFlingData(@PathVariable UUID id) { + public ResponseEntity getFlingData(@PathVariable UUID id) throws IOException { FlingDto flingDto = flingService.getById(id); InputStreamResource data = new InputStreamResource(archiveService.getFling(id)); diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java index 7f27957..be6b682 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/ArtifactDto.java @@ -13,8 +13,8 @@ import lombok.NoArgsConstructor; @Data @Builder -@NoArgsConstructor @AllArgsConstructor +@NoArgsConstructor @Schema(name = "Artifact") public class ArtifactDto { @Schema(accessMode = AccessMode.READ_ONLY, type = "string") diff --git a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java index a34e438..8165f09 100644 --- a/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java +++ b/service/fling/src/main/java/net/friedl/fling/persistence/repositories/ArtifactRepository.java @@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import net.friedl.fling.persistence.entities.ArtifactEntity; public interface ArtifactRepository extends JpaRepository { - List findAllByFlingId(Long flingId); + List findAllByFlingId(UUID flingId); } diff --git a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java index 4a35965..0eadaa2 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/ArtifactService.java @@ -1,5 +1,6 @@ package net.friedl.fling.service; +import java.io.IOException; import java.util.UUID; import javax.transaction.Transactional; import javax.validation.constraints.NotNull; @@ -66,20 +67,11 @@ public class ArtifactService { * Deletes an artifact identified by {@code id}. NOOP if the artifact cannot be found. * * @param id An {@link UUID} that identifies the artifact + * @throws IOException If the deletion failed */ - public void delete(UUID id) { - if (id == null) - return; - - ArtifactEntity artifactEntity = artifactRepository.findById(id).orElse(null); - - if (artifactEntity == null) { - log.warn("Cannot delete artifact {}. Artifact not found.", id); - return; - } - + public void delete(UUID id) throws IOException { archiveService.deleteArtifact(id); - artifactRepository.delete(artifactEntity); - log.info("Deleted artifact {}", artifactEntity); + artifactRepository.deleteById(id); + log.info("Deleted artifact {}", id); } } diff --git a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java index 9cce26e..2e074f2 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java @@ -1,5 +1,6 @@ package net.friedl.fling.service; +import java.io.IOException; import java.security.MessageDigest; import java.util.Base64; import java.util.List; @@ -87,7 +88,7 @@ public class FlingService { return flingMapper.map(flingEntity); } - public void delete(UUID id) { + public void delete(UUID id) throws IOException { archiveService.deleteFling(id); flingRepository.deleteById(id); log.debug("Deleted fling {}", id); @@ -95,6 +96,10 @@ public class FlingService { public boolean validateAuthCode(UUID id, String authCode) { FlingEntity flingEntity = flingRepository.getOne(id); + if(StringUtils.hasText(flingEntity.getAuthCode()) != StringUtils.hasText(authCode)) { + return false; // only one of them is empty; implicit null safety check + } + boolean valid = flingEntity.getAuthCode().equals(hashAuthCode(authCode)); log.debug("Provided authentication for {} is {} valid", id, valid ? "" : "not"); return valid; diff --git a/service/fling/src/main/java/net/friedl/fling/service/ServiceException.java b/service/fling/src/main/java/net/friedl/fling/service/ServiceException.java deleted file mode 100644 index 90abbf2..0000000 --- a/service/fling/src/main/java/net/friedl/fling/service/ServiceException.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.friedl.fling.service; - -public class ServiceException extends Exception { - private static final long serialVersionUID = 2159182914434903969L; - - /** - * {@inheritDoc} - */ - public ServiceException() { - super(); - } - - /** - * {@inheritDoc} - */ - public ServiceException(String message) { - super(message); - } - - /** - * {@inheritDoc} - */ - public ServiceException(String message, Throwable cause) { - super(message, cause); - } - - /** - * {@inheritDoc} - */ - public ServiceException(Throwable cause) { - super(cause); - } - - /** - * {@inheritDoc} - */ - protected ServiceException(String message, Throwable cause, - boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveException.java b/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveException.java deleted file mode 100644 index fda5fd6..0000000 --- a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveException.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.friedl.fling.service.archive; - -public class ArchiveException extends RuntimeException { - private static final long serialVersionUID = 6216735865308056261L; - - /** - * Constructs a new exception with {@code null} as its detail message. The cause is not - * initialized, and may subsequently be initialized by a call to {@link #initCause}. - */ - public ArchiveException() { - super(); - } - - /** - * Constructs a new exception with the specified detail message. The cause is not initialized, and - * may subsequently be initialized by a call to {@link #initCause}. - * - * @param message the detail message. The detail message is saved for later retrieval by the - * {@link #getMessage()} method. - */ - public ArchiveException(String message) { - super(message); - } - - /** - * Constructs a new exception with the specified detail message and cause. - *

- * Note that the detail message associated with {@code cause} is not automatically - * incorporated in this exception's detail message. - * - * @param message the detail message (which is saved for later retrieval by the - * {@link #getMessage()} method). - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). - * (A {@code null} value is permitted, and indicates that the cause is nonexistent or - * unknown.) - * @since 1.4 - */ - public ArchiveException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Constructs a new exception with the specified cause and a detail message of - * {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail - * message of {@code cause}). This constructor is useful for exceptions that are little more than - * wrappers for other throwables (for example, - * {@link java.security.PrivilegedActionArchiveException}). - * - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). - * (A {@code null} value is permitted, and indicates that the cause is nonexistent or - * unknown.) - * @since 1.4 - */ - public ArchiveException(Throwable cause) { - super(cause); - } - - /** - * Constructs a new exception with the specified detail message, cause, suppression enabled or - * disabled, and writable stack trace enabled or disabled. - * - * @param message the detail message. - * @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is - * nonexistent or unknown.) - * @param enableSuppression whether or not suppression is enabled or disabled - * @param writableStackTrace whether or not the stack trace should be writable - * @since 1.7 - */ - protected ArchiveException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java b/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java index 922bed9..f1472c4 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/archive/ArchiveService.java @@ -1,8 +1,8 @@ package net.friedl.fling.service.archive; +import java.io.IOException; import java.io.InputStream; import java.util.UUID; -import java.util.zip.ZipInputStream; /** * Interface for persisting artifacts @@ -16,15 +16,15 @@ public interface ArchiveService { * @param id The artifact id * @return An {@link InputStream} for reading the artifact */ - InputStream getArtifact(UUID artifactId); + InputStream getArtifact(UUID artifactId) throws IOException; /** * Retrieve a packaged fling from the archive * * @param flingId The fling id - * @return An {@link ZipInputStream} representing the fling and its artifacts + * @return An {@link InputStream} representing the fling and its artifacts */ - ZipInputStream getFling(UUID flingId); + InputStream getFling(UUID flingId) throws IOException; /** * Store an artifact @@ -32,19 +32,19 @@ public interface ArchiveService { * @param artifactStream The artifact to store represented as {@link InputStream} * @param artifactId The id of the artifact. Must be an existing artifact in the DB. Not null. */ - void storeArtifact(UUID artifactId, InputStream artifactStream); + void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException; /** * Delete an artifact * * @param id The unique artifact id */ - void deleteArtifact(UUID artifactId); + void deleteArtifact(UUID artifactId) throws IOException; /** * Delete a fling * * @param flingId The unique fling id */ - void deleteFling(UUID flingId); + void deleteFling(UUID flingId) throws IOException; } diff --git a/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java b/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java index ee72eca..ffbb18e 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java +++ b/service/fling/src/main/java/net/friedl/fling/service/archive/impl/FileSystemArchive.java @@ -15,14 +15,12 @@ import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.zip.ZipInputStream; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.transaction.Transactional; import javax.validation.constraints.NotBlank; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Service; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.friedl.fling.persistence.entities.ArtifactEntity; import net.friedl.fling.persistence.repositories.ArtifactRepository; @@ -49,6 +47,7 @@ public class FileSystemArchive implements ArchiveService { public void postConstruct() { try { Files.createDirectories(archivePath); + log.debug("Using archive path {}", archivePath); } catch (IOException e) { log.error("Could not create directory at archive path {}", archivePath); throw new UncheckedIOException(e); @@ -60,6 +59,7 @@ public class FileSystemArchive implements ArchiveService { filesystems.forEach((uri, zfs) -> { try { zfs.close(); + log.debug("Closed {}", uri); } catch (IOException e) { log.error("Could not close file system for {}", uri); } @@ -67,8 +67,7 @@ public class FileSystemArchive implements ArchiveService { } @Override - @SneakyThrows - public InputStream getArtifact(UUID artifactId) { + public InputStream getArtifact(UUID artifactId) throws IOException { log.debug("Reading data for artifact {}", artifactId); FileSystem zipDisk = getZipDisk(artifactId); @@ -79,17 +78,15 @@ public class FileSystemArchive implements ArchiveService { } @Override - @SneakyThrows - public ZipInputStream getFling(UUID flingId) { + public InputStream getFling(UUID flingId) throws IOException { log.debug("Reading data for fling {}", flingId); Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip"); log.debug("Zip disk path is {}", zipDiskPath); - return new ZipInputStream(new FileInputStream(zipDiskPath.toFile())); + return new FileInputStream(zipDiskPath.toFile()); } @Override - @SneakyThrows - public void storeArtifact(UUID artifactId, InputStream artifactStream) { + public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException { log.debug("Storing artifact {}", artifactId); setArchived(artifactId, false); @@ -103,8 +100,7 @@ public class FileSystemArchive implements ArchiveService { } @Override - @SneakyThrows - public void deleteArtifact(UUID artifactId) { + public void deleteArtifact(UUID artifactId) throws IOException { log.debug("Deleting artifact {}", artifactId); FileSystem zipDisk = getZipDisk(artifactId); Files.delete(getZipDiskPath(artifactId, zipDisk)); @@ -115,8 +111,7 @@ public class FileSystemArchive implements ArchiveService { } @Override - @SneakyThrows - public void deleteFling(UUID flingId) { + public void deleteFling(UUID flingId) throws IOException { URI zipDiskUri = resolveFlingUri(flingId); log.debug("Closing zip disk at {}", zipDiskUri); @@ -135,8 +130,9 @@ public class FileSystemArchive implements ArchiveService { Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip"); log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath); Files.delete(zipDiskPath); + + artifactRepository.findAllByFlingId(flingId).forEach(ar -> ar.setArchived(false)); } - } private void setArchived(UUID artifactId, boolean archived) { @@ -168,7 +164,7 @@ public class FileSystemArchive implements ArchiveService { URI uri = resolveArtifactUri(artifactId); log.debug("Looking for zip disk at uri {}", uri); - // make sure nobody opens closes, deletes or interleavingly opens the filesystem while it is + // make sure nobody closes, deletes or interleavingly opens the filesystem while it is // being opened synchronized (filesystems) { if (!filesystems.containsKey(uri)) { @@ -214,4 +210,9 @@ public class FileSystemArchive implements ArchiveService { public void setArchivePath(String archivePath) { this.archivePath = Paths.get(archivePath); } + + public void setArchivePath(Path archivePath) { + this.archivePath = archivePath; + } + } diff --git a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java index 32ef8b7..c87264e 100644 --- a/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java +++ b/service/fling/src/test/java/net/friedl/fling/controller/ArtifactControllerTest.java @@ -1,9 +1,37 @@ package net.friedl.fling.controller; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.UUID; +import javax.persistence.EntityNotFoundException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import net.friedl.fling.model.dto.ArtifactDto; +import net.friedl.fling.service.ArtifactService; +import net.friedl.fling.service.archive.ArchiveService; @WebMvcTest(controllers = ArtifactController.class, // do auto-configure security @@ -11,34 +39,102 @@ import org.springframework.context.annotation.FilterType; // do not try to create beans in security excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*")) class ArtifactControllerTest { -// @Autowired -// private MockMvc mvc; -// -// @MockBean -// private ArtifactService artifactService; -// -// @Test -// public void testGetArtifacts_noArtifacts_empty() throws Exception { -//// Long flingId = 123L; -//// -//// when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of()); -//// -//// mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) -//// .andExpect(jsonPath("$", hasSize(0))); -// } -// -// @Test -// public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception { -//// Long flingId = 123L; -//// String artifactName = "TEST"; -//// -//// ArtifactDto artifactDto = new ArtifactDto(); -//// artifactDto.setName(artifactName); -//// -//// when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto)); -//// -//// mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) -//// .andExpect(jsonPath("$", hasSize(1))) -//// .andExpect(jsonPath("$[0].name", equalTo(artifactName))); -// } + @Autowired + private MockMvc mvc; + + @MockBean + private ArtifactService artifactService; + + @MockBean + private ArchiveService archiveService; + + private static final UUID ARTIFACT_ID = UUID.randomUUID(); + + private ArtifactDto artifactDto = + new ArtifactDto(ARTIFACT_ID, Path.of("testArtifact"), Instant.EPOCH, false); + + @Test + public void getArtifact_noArtifactWithId_notFound() throws Exception { + when(artifactService.getById(ARTIFACT_ID)).thenThrow(EntityNotFoundException.class); + + mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void getArtifacts_ok() throws Exception { + when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto); + + mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", equalTo(ARTIFACT_ID.toString()))); + } + + @Test + public void deleteArtifact_noArtifactWithId_notFound() throws Exception { + doThrow(EntityNotFoundException.class).when(artifactService).delete(ARTIFACT_ID); + + mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID)) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteArtifact_ok() throws Exception { + mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID)) + .andExpect(status().isOk()); + } + + @Test + public void uploadArtifact_ioError_serverError() throws Exception { + doThrow(IOException.class).when(archiveService).storeArtifact(any(), any()); + + byte[] payload = "Payload".getBytes(); + mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .content(payload)) + .andExpect(status().isInternalServerError()); + } + + @Test + public void uploadArtifact_ok() throws Exception { + byte[] payload = "Payload".getBytes(); + mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .content(payload)) + .andExpect(status().isOk()); + } + + @Test + public void downloadArtifact_noArtifact_notFound() throws Exception { + doThrow(EntityNotFoundException.class).when(artifactService).getById(ARTIFACT_ID); + + mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID)) + .andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION)) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) + .andExpect(status().isNotFound()); + } + + @Test + public void downloadArtifact_ioError_serverError() throws Exception { + doThrow(IOException.class).when(archiveService).getArtifact(ARTIFACT_ID); + + mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID)) + .andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION)) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) + .andExpect(status().isInternalServerError()); + } + + @Test + public void downloadArtifact_ok() throws Exception { + when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto); + byte[] testData = "test".getBytes(); + when(archiveService.getArtifact(any())).thenReturn(new ByteArrayInputStream(testData)); + + mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID)) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION)) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, + Matchers.containsString("attachment;filename"))) + .andExpect(content().bytes(testData)); + } } diff --git a/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java new file mode 100644 index 0000000..3d0e80c --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java @@ -0,0 +1,206 @@ +package net.friedl.fling.controller; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import javax.persistence.EntityNotFoundException; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.friedl.fling.model.dto.ArtifactDto; +import net.friedl.fling.model.dto.FlingDto; +import net.friedl.fling.service.ArtifactService; +import net.friedl.fling.service.FlingService; +import net.friedl.fling.service.archive.ArchiveService; + +@WebMvcTest(controllers = FlingController.class, + // do auto-configure security + excludeAutoConfiguration = SecurityAutoConfiguration.class, + // do not try to create beans in security + excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*")) +public class FlingControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper mapper; + + @MockBean + private FlingService flingService; + + @MockBean + private ArtifactService artifactService; + + @MockBean + private ArchiveService archiveService; + + private static final UUID flingId = UUID.randomUUID(); + + private FlingDto flingDto = new FlingDto(flingId, "name", Instant.EPOCH, "shareId", "authCode", + false, true, true, 1, null); + + private ArtifactDto artifactDto = + new ArtifactDto(UUID.randomUUID(), Path.of("testArtifact"), Instant.EPOCH, false); + + @Test + public void getFlings_noFlings_empty() throws Exception { + when(flingService.findAll()).thenReturn(List.of()); + + mockMvc.perform(get("/api/fling")) + .andExpect(jsonPath("$", hasSize(0))) + .andExpect(status().isOk()); + } + + @Test + public void getFlings_allFlings() throws Exception { + when(flingService.findAll()).thenReturn(List.of(flingDto, flingDto)); + + mockMvc.perform(get("/api/fling")) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].id", equalTo(flingId.toString()))) + .andExpect(status().isOk()); + } + + @Test + public void postFling_ok() throws Exception { + mockMvc.perform(post("/api/fling") + .content(mapper.writeValueAsString(flingDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void postArtifact_ok() throws Exception { + mockMvc.perform(post("/api/fling/{id}/artifact", flingId) + .content(mapper.writeValueAsString(artifactDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + public void getFling_noFlingWithId_notFound() throws Exception { + doThrow(EntityNotFoundException.class).when(flingService).getById(flingId); + + mockMvc.perform(get("/api/fling/{id}", flingId)) + .andExpect(status().isNotFound()); + } + + @Test + public void getFling_flingFound_returnsFling() throws Exception { + when(flingService.getById(flingId)).thenReturn(flingDto); + + mockMvc.perform(get("/api/fling/{id}", flingId)) + .andExpect(jsonPath("$.id", equalTo(flingId.toString()))) + .andExpect(status().isOk()); + } + + @Test + public void getFlingByShareId_noFlingWithShareId_notFound() throws Exception { + doThrow(EntityNotFoundException.class).when(flingService).getByShareId("doesNotExist"); + + mockMvc.perform(get("/api/fling/share/{shareId}", "doesNotExist")) + .andExpect(status().isNotFound()); + } + + @Test + public void getFlingByShareId_flingFind_returnsFling() throws Exception { + doReturn(flingDto).when(flingService).getByShareId("shareId"); + + mockMvc.perform(get("/api/fling/share/{shareId}", "shareId")) + .andExpect(jsonPath("$.id", equalTo(flingId.toString()))) + .andExpect(status().isOk()); + } + + @Test + public void deleteFling_noFlingWithId_notFound() throws Exception { + doThrow(EntityNotFoundException.class).when(flingService).delete(flingId); + + mockMvc.perform(delete("/api/fling/{id}", flingId)) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteFling_ok() throws Exception { + doNothing().when(flingService).delete(flingId); + + mockMvc.perform(delete("/api/fling/{id}", flingId)) + .andExpect(status().isOk()); + } + + @Test + public void getFlingData_ioError_serverError() throws Exception { + doThrow(IOException.class).when(archiveService).getFling(flingId); + + mockMvc.perform(get("/api/fling/{id}/data", flingId)) + .andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION)) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, + not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE)))) + .andExpect(status().isInternalServerError()); + } + + @Test + public void getFlingData_ok() throws Exception { + when(flingService.getById(flingId)).thenReturn(flingDto); + int[] testZipInt = new int[] { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x77, 0xe4, 0x50, 0xc6, + 0x35, + 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x74, + 0x65, + 0x73, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x37, 0x7d, 0x00, 0x5f, + 0x75, + 0x78, 0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x74, + 0x65, + 0x73, 0x74, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x76, + 0x77, 0xe4, 0x50, 0xc6, 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x04, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xb4, 0x81, 0x00, 0x00, + 0x00, + 0x00, 0x74, 0x65, 0x73, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x75, + 0x78, + 0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x50, 0x4b, + 0x05, + 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x43, 0x00, + 0x00, + 0x00, 0x00, 0x00 + }; + byte[] testZip = new byte[testZipInt.length]; + for (int idx = 0; idx < testZip.length; idx++) testZip[idx] = (byte) testZipInt[idx]; + + when(archiveService.getFling(any())).thenReturn(new ByteArrayInputStream(testZip)); + + mockMvc.perform(get("/api/fling/{id}/data", flingId)) + .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION)) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, + Matchers.containsString("attachment;filename"))) + .andExpect(content().bytes(testZip)); + } +} diff --git a/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java new file mode 100644 index 0000000..95c49de --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/service/ArtifactServiceTest.java @@ -0,0 +1,137 @@ +package net.friedl.fling.service; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import net.friedl.fling.model.dto.ArtifactDto; +import net.friedl.fling.model.mapper.ArtifactMapper; +import net.friedl.fling.model.mapper.ArtifactMapperImpl; +import net.friedl.fling.persistence.entities.ArtifactEntity; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.ArtifactRepository; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.service.archive.ArchiveService; + +@ExtendWith(SpringExtension.class) +public class ArtifactServiceTest { + + @Autowired + private ArtifactService artifactService; + + @MockBean + private FlingRepository flingRepository; + + @MockBean + private ArtifactRepository artifactRepository; + + @MockBean + private ArchiveService archiveService; + + private FlingEntity flingEntity; + + private ArtifactEntity artifactEntity1; + + private ArtifactEntity artifactEntity2; + + @TestConfiguration + static class FlingServiceTestConfiguration { + @Bean + public ArtifactMapper artifactMapper() { + return new ArtifactMapperImpl(); + } + + @Bean + public ArtifactService artifactService(ArtifactRepository artifactRepository, + FlingRepository flingRepository, ArtifactMapper artifactMapper, + ArchiveService archiveService) { + + return new ArtifactService(artifactRepository, flingRepository, artifactMapper, archiveService); + } + } + + @BeforeEach + public void beforeEach() { + this.artifactEntity1 = new ArtifactEntity(); + artifactEntity1.setId(UUID.randomUUID()); + artifactEntity1.setUploadTime(Instant.EPOCH); + artifactEntity1.setPath(Path.of("artifact1")); + + this.artifactEntity2 = new ArtifactEntity(); + artifactEntity2.setId(UUID.randomUUID()); + artifactEntity2.setUploadTime(Instant.EPOCH.plus(12000, ChronoUnit.DAYS)); + artifactEntity2.setPath(Path.of("/","/sub","artifact2")); + + this.flingEntity = new FlingEntity(); + flingEntity.setId(UUID.randomUUID()); + flingEntity.setName("fling"); + flingEntity.setCreationTime(Instant.now()); + + when(flingRepository.save(any())).then(new Answer() { + @Override + public FlingEntity answer(InvocationOnMock invocation) throws Throwable { + FlingEntity flingEntity = invocation.getArgument(0); + if(flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID()); + return flingEntity; + } + }); + + when(artifactRepository.save(any())).then(new Answer() { + @Override + public ArtifactEntity answer(InvocationOnMock invocation) throws Throwable { + ArtifactEntity artifactEntity = invocation.getArgument(0); + artifactEntity.setId(UUID.randomUUID()); + return artifactEntity; + } + }); + + } + + @Test + public void getById_artifactExists_ok() { + when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); + + ArtifactDto artifactDto = artifactService.getById(artifactEntity1.getId()); + assertThat(artifactDto.getId(), equalTo(artifactEntity1.getId())); + assertThat(artifactDto.getPath(), equalTo(artifactEntity1.getPath())); + assertThat(artifactDto.getUploadTime(), equalTo(artifactEntity1.getUploadTime())); + } + + @Test + public void create_createsArtifact_ok() { + ArtifactDto artifactToCreate = ArtifactDto.builder() + .uploadTime(Instant.now()) + .path(Path.of("new", "artifacts")) + .build(); + + ArtifactDto createdArtifact = artifactService.create(flingEntity.getId(), artifactToCreate); + + assertThat(createdArtifact.getUploadTime(), equalTo(artifactToCreate.getUploadTime())); + assertThat(createdArtifact.getPath(), equalTo(artifactToCreate.getPath())); + } + + @Test + public void delete_deletesArchiveAndArtifactEntry() throws IOException { + artifactService.delete(artifactEntity1.getId()); + + verify(archiveService).deleteArtifact(artifactEntity1.getId()); + verify(artifactRepository).deleteById(artifactEntity1.getId()); + } + +} diff --git a/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java b/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java new file mode 100644 index 0000000..df1d1a7 --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/service/FlingServiceTest.java @@ -0,0 +1,181 @@ +package net.friedl.fling.service; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import net.friedl.fling.model.dto.FlingDto; +import net.friedl.fling.model.mapper.FlingMapper; +import net.friedl.fling.model.mapper.FlingMapperImpl; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.FlingRepository; +import net.friedl.fling.service.archive.ArchiveService; + +@ExtendWith(SpringExtension.class) +public class FlingServiceTest { + @Autowired + private FlingService flingService; + + @Autowired + private FlingMapper flingMapper; + + @Autowired + private MessageDigest keyHashDigest; + + @MockBean + private FlingRepository flingRepository; + + @MockBean + private ArchiveService archiveService; + + private FlingEntity flingEntity1; + + private FlingEntity flingEntity2; + + @TestConfiguration + static class FlingServiceTestConfiguration { + @Bean + public FlingMapper flingMapper() { + return new FlingMapperImpl(); + } + + @Bean + public MessageDigest keyHashDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-512"); + } + + @Bean + public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper, + ArchiveService archiveService, MessageDigest keyHashDigest) { + return new FlingService(flingRepository, flingMapper, archiveService, keyHashDigest); + } + } + + @BeforeEach + public void beforeEach() { + this.flingEntity1 = new FlingEntity(); + flingEntity1.setId(UUID.randomUUID()); + flingEntity1.setName("fling1"); + flingEntity1.setAuthCode(new String(Hex.encode(keyHashDigest.digest("authCode1".getBytes())))); + flingEntity1.setCreationTime(Instant.now()); + + this.flingEntity2 = new FlingEntity(); + flingEntity2.setId(UUID.randomUUID()); + flingEntity2.setName("fling2"); + flingEntity2.setShareId("shareId2"); + flingEntity2.setCreationTime(Instant.now()); + + when(flingRepository.save(any())).then(new Answer() { + @Override + public FlingEntity answer(InvocationOnMock invocation) throws Throwable { + FlingEntity flingEntity = invocation.getArgument(0); + flingEntity.setId(UUID.randomUUID()); + return flingEntity; + }}); + } + + @Test + public void findAll_noFlings_empty() { + when(flingRepository.findAll()).thenReturn(List.of()); + + assertThat(flingService.findAll(), is(empty())); + } + + @Test + public void findAll_hasFlings_allFlings() { + when(flingRepository.findAll()).thenReturn(List.of(flingEntity1, flingEntity2)); + + assertThat(flingService.findAll(), hasItems( + flingMapper.map(flingEntity1), flingMapper.map(flingEntity2))); + } + + @Test + public void getById_flingDto() { + when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1); + assertThat(flingService.getById(flingEntity1.getId()), equalTo(flingMapper.map(flingEntity1))); + } + + @Test + public void create_emptyFling_defaultValues() { + FlingDto flingDto = new FlingDto(); + + FlingDto createdFling = flingService.create(flingDto); + assertThat(createdFling.getShareId(), not(emptyOrNullString())); + assertThat(createdFling.getAuthCode(), emptyOrNullString()); + } + + @Test + public void create_hasAuthCode_setAuthCode() { + FlingDto flingDto = new FlingDto(); + flingDto.setAuthCode("test"); + + String hashedAuthCode = new String(Hex.encode(keyHashDigest.digest(flingDto.getAuthCode().getBytes()))); + + FlingDto createdFling = flingService.create(flingDto); + assertThat(createdFling.getAuthCode(), is(hashedAuthCode)); + } + + @Test + public void create_hasShareId_setShareId() { + FlingDto flingDto = new FlingDto(); + flingDto.setShareId("test"); + + FlingDto createdFling = flingService.create(flingDto); + assertThat(createdFling.getShareId(), is("test")); + } + + @Test + public void getByShareId_flingDto() { + when(flingRepository.findByShareId("shareId2")).thenReturn(flingEntity2); + + FlingDto foundFling = flingService.getByShareId("shareId2"); + assertThat(foundFling.getShareId(), equalTo("shareId2")); + } + + @Test + public void delete_deletesFromArchiveAndDb() throws IOException { + UUID testId = UUID.randomUUID(); + flingService.delete(testId); + + verify(archiveService).deleteFling(testId); + verify(flingRepository).deleteById(testId); + } + + @Test + public void validateAuthCode_codesMatch_true() { + when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1); + + assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true)); + } + + @Test + public void validateAuthCode_codesDoNotMatch_false() { + when(flingRepository.getOne(flingEntity2.getId())).thenReturn(flingEntity2); + + assertThat(flingService.validateAuthCode(flingEntity2.getId(), "authCode1"), is(false)); + } + +} diff --git a/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java b/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java new file mode 100644 index 0000000..fc34e53 --- /dev/null +++ b/service/fling/src/test/java/net/friedl/fling/service/archive/FileSystemArchiveTest.java @@ -0,0 +1,300 @@ +package net.friedl.fling.service.archive; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import net.friedl.fling.persistence.entities.ArtifactEntity; +import net.friedl.fling.persistence.entities.FlingEntity; +import net.friedl.fling.persistence.repositories.ArtifactRepository; +import net.friedl.fling.service.archive.impl.FileSystemArchive; + +@ExtendWith(SpringExtension.class) +public class FileSystemArchiveTest { + @Autowired + private FileSystemArchive fileSystemArchive; + + @MockBean + private ArtifactRepository artifactRepository; + + private FlingEntity flingEntity1; + + private FlingEntity flingEntity2; + + private ArtifactEntity artifactEntity1; + + private ArtifactEntity artifactEntity2; + + @TempDir + static Path tempDir; + + @TestConfiguration + static class FlingServiceTestConfiguration { + @Bean + public FileSystemArchive fileSystemArchive(ArtifactRepository artifactRepository) + throws URISyntaxException { + FileSystemArchive fileSystemArchive = new FileSystemArchive(artifactRepository); + fileSystemArchive.setArchivePath(tempDir); + return fileSystemArchive; + } + } + + @BeforeEach + public void beforeEach() throws IOException, URISyntaxException { + repopulateArchivePath(); + setupTestEntites(); + } + + @Test + public void getArtifact_flingDiskForFlingIdDoesNotExist_throws() { + flingEntity1.setId(UUID.randomUUID()); + artifactEntity1.setFling(flingEntity1); + when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); + + assertThrows(IOException.class, () -> fileSystemArchive.getArtifact(artifactEntity1.getId())); + } + + @Test + public void getArtifact_returnsArtifact() throws IOException { + when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); + + InputStream expectedArtifact = + getClass().getClassLoader().getResourceAsStream("filesystem/artifacts/artifact1"); + byte[] expectedArtifactData = expectedArtifact.readAllBytes(); + expectedArtifact.close(); + + InputStream retrievedArtifact = fileSystemArchive.getArtifact(artifactEntity1.getId()); + byte[] retrievedArtifactData = retrievedArtifact.readAllBytes(); + retrievedArtifact.close(); + + assertThat(retrievedArtifactData, equalTo(expectedArtifactData)); + } + + @Test + public void getFling_doesNotExist_throws() { + assertThrows(IOException.class, () -> fileSystemArchive.getFling(UUID.randomUUID())); + } + + @Test + public void getFling_returnsFling() throws IOException { + UUID flingUUID = new UUID(0, 0); + InputStream expectedFling = getClass().getClassLoader() + .getResourceAsStream("filesystem/archive_path/" + flingUUID.toString() + ".zip"); + byte[] expectedFlingData = expectedFling.readAllBytes(); + expectedFling.close(); + + InputStream retrievedFling = fileSystemArchive.getFling(flingUUID); + byte[] retrievedFlingData = retrievedFling.readAllBytes(); + retrievedFling.close(); + + assertThat(retrievedFlingData, equalTo(expectedFlingData)); + } + + @Test + public void deleteArtifact_setsArchivedFalse() throws IOException { + when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); + + fileSystemArchive.deleteArtifact(artifactEntity1.getId()); + + assertThat(artifactEntity1.getArchived(), equalTo(false)); + + InputStream flingStream = + new FileInputStream( + tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile()); + ZipInputStream zis = new ZipInputStream(flingStream); + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString()))); + zis.closeEntry(); + } + zis.close(); + } + + @Test + public void deleteArtifact_deletesArtifactFromZipDisk() throws IOException { + when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1); + + fileSystemArchive.deleteArtifact(artifactEntity1.getId()); + + InputStream flingStream = + new FileInputStream( + tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile()); + ZipInputStream zis = new ZipInputStream(flingStream); + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString()))); + zis.closeEntry(); + } + zis.close(); + } + + @Test + public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException { + InputStream artifact2Stream = new FileInputStream( + new File(getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); + when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2); + + fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream); + + artifact2Stream.close(); + + assertThat(artifactEntity2.getArchived(), equalTo(true)); + } + + @Test + public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException { + InputStream artifact2Stream = new FileInputStream( + new File(getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI())); + when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2); + + fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream); + + artifact2Stream.close(); + + InputStream flingStream = + new FileInputStream( + tempDir.resolve(artifactEntity2.getFling().getId().toString() + ".zip").toFile()); + ZipInputStream zis = new ZipInputStream(flingStream); + ZipEntry zipEntry; + List diskEntries = new LinkedList<>(); + while ((zipEntry = zis.getNextEntry()) != null) { + diskEntries.add(zipEntry.getName()); + zis.closeEntry(); + } + zis.close(); + + assertThat(diskEntries, hasItem(Path.of("/").relativize(artifactEntity2.getPath()).toString())); + } + + @Test + public void deleteFling_setsArchivedFalseForAllContainedArtifacts() throws IOException { + when(artifactRepository.findAllByFlingId(artifactEntity1.getFling().getId())) + .thenReturn(List.of(artifactEntity1)); + + fileSystemArchive.deleteFling(artifactEntity1.getFling().getId()); + + assertThat(artifactEntity1.getArchived(), equalTo(false)); + } + + @Test + public void deleteFling_deletesZipDisk() throws IOException { + assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")), + equalTo(true)); + + fileSystemArchive.deleteFling(artifactEntity1.getFling().getId()); + + assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")), + equalTo(false)); + } + + private void repopulateArchivePath() throws IOException, URISyntaxException { + Files.walkFileTree(tempDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + + Path source = + Path.of(getClass().getClassLoader().getResource("filesystem/archive_path").toURI()); + Path target = tempDir; + Files.walkFileTree(source, Set.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, + new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + Path targetdir = target.resolve(source.relativize(dir)); + try { + Files.copy(dir, targetdir); + } catch (FileAlreadyExistsException e) { + if (!Files.isDirectory(targetdir)) + throw e; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.copy(file, target.resolve(source.relativize(file))); + return FileVisitResult.CONTINUE; + } + }); + } + + private void setupTestEntites() { + // Fling1/Artifact1 + this.artifactEntity1 = new ArtifactEntity(); + artifactEntity1.setId(UUID.randomUUID()); + artifactEntity1.setUploadTime(Instant.EPOCH); + artifactEntity1.setPath(Path.of("artifact1")); + artifactEntity1.setArchived(true); + + this.flingEntity1 = new FlingEntity(); + flingEntity1.setId(new UUID(0, 0)); + flingEntity1.setName("fling1"); + flingEntity1.setCreationTime(Instant.now()); + + artifactEntity1.setFling(flingEntity1); + + + // Fling2/Artifact2 + this.artifactEntity2 = new ArtifactEntity(); + artifactEntity2.setId(UUID.randomUUID()); + artifactEntity2.setUploadTime(Instant.EPOCH.plus(12000, ChronoUnit.DAYS)); + artifactEntity2.setPath(Path.of("/", "/sub", "artifact2")); + artifactEntity2.setArchived(false); + + this.flingEntity2 = new FlingEntity(); + flingEntity2.setId(new UUID(1, 0)); + flingEntity2.setName("fling2"); + flingEntity2.setCreationTime(Instant.EPOCH); + + artifactEntity2.setFling(flingEntity2); + } +} diff --git a/service/fling/src/test/resources/filesystem/archive_path/.keep b/service/fling/src/test/resources/filesystem/archive_path/.keep new file mode 100644 index 0000000..e69de29 diff --git a/service/fling/src/test/resources/filesystem/artifacts/artifact1 b/service/fling/src/test/resources/filesystem/artifacts/artifact1 new file mode 100644 index 0000000..5105e15 --- /dev/null +++ b/service/fling/src/test/resources/filesystem/artifacts/artifact1 @@ -0,0 +1 @@ +artifact1 ok diff --git a/service/fling/src/test/resources/filesystem/artifacts/artifact2 b/service/fling/src/test/resources/filesystem/artifacts/artifact2 new file mode 100644 index 0000000..59dbf5e --- /dev/null +++ b/service/fling/src/test/resources/filesystem/artifacts/artifact2 @@ -0,0 +1 @@ +artifact2 ok