This commit is contained in:
parent
2b18120c0b
commit
a66b51e1e5
20 changed files with 1045 additions and 206 deletions
|
@ -20,7 +20,7 @@ public class FlingConfiguration {
|
||||||
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
||||||
return MessageDigest.getInstance("SHA-512");
|
return MessageDigest.getInstance("SHA-512");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ObjectMapper objectMapper() {
|
public ObjectMapper objectMapper() {
|
||||||
SimpleModule simpleModule = new SimpleModule();
|
SimpleModule simpleModule = new SimpleModule();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package net.friedl.fling.controller;
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.friedl.fling.model.dto.ArtifactDto;
|
import net.friedl.fling.model.dto.ArtifactDto;
|
||||||
import net.friedl.fling.service.ArtifactService;
|
import net.friedl.fling.service.ArtifactService;
|
||||||
import net.friedl.fling.service.archive.ArchiveService;
|
import net.friedl.fling.service.archive.ArchiveService;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/artifacts")
|
@RequestMapping("/api/artifacts")
|
||||||
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
||||||
|
@ -42,36 +38,28 @@ public class ArtifactController {
|
||||||
this.archiveService = archiveService;
|
this.archiveService = archiveService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiResponse(responseCode = "404", description = "No artifact with `id` found")
|
||||||
@GetMapping(path = "/{id}")
|
@GetMapping(path = "/{id}")
|
||||||
public ArtifactDto getArtifact(@PathVariable UUID id) {
|
public ArtifactDto getArtifact(@PathVariable UUID id) {
|
||||||
return artifactService.getById(id);
|
return artifactService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiResponse(responseCode = "404", description = "No artifact with `id` found")
|
||||||
@DeleteMapping(path = "/{id}")
|
@DeleteMapping(path = "/{id}")
|
||||||
public void deleteArtifact(@PathVariable UUID id) {
|
public void deleteArtifact(@PathVariable UUID id) throws IOException {
|
||||||
artifactService.delete(id);
|
artifactService.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(requestBody = @RequestBody(
|
@RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary")))
|
||||||
content = @Content(schema = @Schema(type = "string", format = "binary"))))
|
|
||||||
@PostMapping(path = "/{id}/data")
|
@PostMapping(path = "/{id}/data")
|
||||||
public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) {
|
public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request) throws IOException {
|
||||||
try {
|
|
||||||
archiveService.storeArtifact(id, request.getInputStream());
|
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",
|
||||||
@ApiResponse(responseCode = "200",
|
content = @Content(schema = @Schema(type = "string", format = "binary")))
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
|
|
||||||
schema = @Schema(type = "string", format = "binary")))
|
|
||||||
})
|
|
||||||
@GetMapping(path = "/{id}/data")
|
@GetMapping(path = "/{id}/data")
|
||||||
public ResponseEntity<Resource> downloadArtifact(@PathVariable UUID id) {
|
public ResponseEntity<Resource> downloadArtifact(@PathVariable UUID id) throws IOException {
|
||||||
ArtifactDto artifactDto = artifactService.getById(id);
|
ArtifactDto artifactDto = artifactService.getById(id);
|
||||||
InputStreamResource data = new InputStreamResource(archiveService.getArtifact(id));
|
InputStreamResource data = new InputStreamResource(archiveService.getArtifact(id));
|
||||||
|
|
||||||
|
|
|
@ -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<Object> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package net.friedl.fling.controller;
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -70,7 +71,7 @@ public class FlingController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public void deleteFling(@PathVariable UUID id) {
|
public void deleteFling(@PathVariable UUID id) throws IOException {
|
||||||
flingService.delete(id);
|
flingService.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ public class FlingController {
|
||||||
schema = @Schema(type = "string", format = "binary")))
|
schema = @Schema(type = "string", format = "binary")))
|
||||||
})
|
})
|
||||||
@GetMapping(path = "/{id}/data")
|
@GetMapping(path = "/{id}/data")
|
||||||
public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) {
|
public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) throws IOException {
|
||||||
FlingDto flingDto = flingService.getById(id);
|
FlingDto flingDto = flingService.getById(id);
|
||||||
InputStreamResource data = new InputStreamResource(archiveService.getFling(id));
|
InputStreamResource data = new InputStreamResource(archiveService.getFling(id));
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,8 @@ import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
@Schema(name = "Artifact")
|
@Schema(name = "Artifact")
|
||||||
public class ArtifactDto {
|
public class ArtifactDto {
|
||||||
@Schema(accessMode = AccessMode.READ_ONLY, type = "string")
|
@Schema(accessMode = AccessMode.READ_ONLY, type = "string")
|
||||||
|
|
|
@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
|
|
||||||
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, UUID> {
|
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, UUID> {
|
||||||
List<ArtifactEntity> findAllByFlingId(Long flingId);
|
List<ArtifactEntity> findAllByFlingId(UUID flingId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import javax.validation.constraints.NotNull;
|
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.
|
* Deletes an artifact identified by {@code id}. NOOP if the artifact cannot be found.
|
||||||
*
|
*
|
||||||
* @param id An {@link UUID} that identifies the artifact
|
* @param id An {@link UUID} that identifies the artifact
|
||||||
|
* @throws IOException If the deletion failed
|
||||||
*/
|
*/
|
||||||
public void delete(UUID id) {
|
public void delete(UUID id) throws IOException {
|
||||||
if (id == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ArtifactEntity artifactEntity = artifactRepository.findById(id).orElse(null);
|
|
||||||
|
|
||||||
if (artifactEntity == null) {
|
|
||||||
log.warn("Cannot delete artifact {}. Artifact not found.", id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
archiveService.deleteArtifact(id);
|
archiveService.deleteArtifact(id);
|
||||||
artifactRepository.delete(artifactEntity);
|
artifactRepository.deleteById(id);
|
||||||
log.info("Deleted artifact {}", artifactEntity);
|
log.info("Deleted artifact {}", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -87,7 +88,7 @@ public class FlingService {
|
||||||
return flingMapper.map(flingEntity);
|
return flingMapper.map(flingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(UUID id) {
|
public void delete(UUID id) throws IOException {
|
||||||
archiveService.deleteFling(id);
|
archiveService.deleteFling(id);
|
||||||
flingRepository.deleteById(id);
|
flingRepository.deleteById(id);
|
||||||
log.debug("Deleted fling {}", id);
|
log.debug("Deleted fling {}", id);
|
||||||
|
@ -95,6 +96,10 @@ public class FlingService {
|
||||||
|
|
||||||
public boolean validateAuthCode(UUID id, String authCode) {
|
public boolean validateAuthCode(UUID id, String authCode) {
|
||||||
FlingEntity flingEntity = flingRepository.getOne(id);
|
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));
|
boolean valid = flingEntity.getAuthCode().equals(hashAuthCode(authCode));
|
||||||
log.debug("Provided authentication for {} is {} valid", id, valid ? "" : "not");
|
log.debug("Provided authentication for {} is {} valid", id, valid ? "" : "not");
|
||||||
return valid;
|
return valid;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
* <p>
|
|
||||||
* Note that the detail message associated with {@code cause} is <i>not</i> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
package net.friedl.fling.service.archive;
|
package net.friedl.fling.service.archive;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for persisting artifacts
|
* Interface for persisting artifacts
|
||||||
|
@ -16,15 +16,15 @@ public interface ArchiveService {
|
||||||
* @param id The artifact id
|
* @param id The artifact id
|
||||||
* @return An {@link InputStream} for reading the artifact
|
* @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
|
* Retrieve a packaged fling from the archive
|
||||||
*
|
*
|
||||||
* @param flingId The fling id
|
* @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
|
* Store an artifact
|
||||||
|
@ -32,19 +32,19 @@ public interface ArchiveService {
|
||||||
* @param artifactStream The artifact to store represented as {@link InputStream}
|
* @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.
|
* @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
|
* Delete an artifact
|
||||||
*
|
*
|
||||||
* @param id The unique artifact id
|
* @param id The unique artifact id
|
||||||
*/
|
*/
|
||||||
void deleteArtifact(UUID artifactId);
|
void deleteArtifact(UUID artifactId) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a fling
|
* Delete a fling
|
||||||
*
|
*
|
||||||
* @param flingId The unique fling id
|
* @param flingId The unique fling id
|
||||||
*/
|
*/
|
||||||
void deleteFling(UUID flingId);
|
void deleteFling(UUID flingId) throws IOException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,12 @@ import java.nio.file.StandardOpenOption;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||||
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
import net.friedl.fling.persistence.repositories.ArtifactRepository;
|
||||||
|
@ -49,6 +47,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
public void postConstruct() {
|
public void postConstruct() {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(archivePath);
|
Files.createDirectories(archivePath);
|
||||||
|
log.debug("Using archive path {}", archivePath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Could not create directory at archive path {}", archivePath);
|
log.error("Could not create directory at archive path {}", archivePath);
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
|
@ -60,6 +59,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
filesystems.forEach((uri, zfs) -> {
|
filesystems.forEach((uri, zfs) -> {
|
||||||
try {
|
try {
|
||||||
zfs.close();
|
zfs.close();
|
||||||
|
log.debug("Closed {}", uri);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Could not close file system for {}", uri);
|
log.error("Could not close file system for {}", uri);
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public InputStream getArtifact(UUID artifactId) throws IOException {
|
||||||
public InputStream getArtifact(UUID artifactId) {
|
|
||||||
log.debug("Reading data for artifact {}", artifactId);
|
log.debug("Reading data for artifact {}", artifactId);
|
||||||
|
|
||||||
FileSystem zipDisk = getZipDisk(artifactId);
|
FileSystem zipDisk = getZipDisk(artifactId);
|
||||||
|
@ -79,17 +78,15 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public InputStream getFling(UUID flingId) throws IOException {
|
||||||
public ZipInputStream getFling(UUID flingId) {
|
|
||||||
log.debug("Reading data for fling {}", flingId);
|
log.debug("Reading data for fling {}", flingId);
|
||||||
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
||||||
log.debug("Zip disk path is {}", zipDiskPath);
|
log.debug("Zip disk path is {}", zipDiskPath);
|
||||||
return new ZipInputStream(new FileInputStream(zipDiskPath.toFile()));
|
return new FileInputStream(zipDiskPath.toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException {
|
||||||
public void storeArtifact(UUID artifactId, InputStream artifactStream) {
|
|
||||||
log.debug("Storing artifact {}", artifactId);
|
log.debug("Storing artifact {}", artifactId);
|
||||||
|
|
||||||
setArchived(artifactId, false);
|
setArchived(artifactId, false);
|
||||||
|
@ -103,8 +100,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public void deleteArtifact(UUID artifactId) throws IOException {
|
||||||
public void deleteArtifact(UUID artifactId) {
|
|
||||||
log.debug("Deleting artifact {}", artifactId);
|
log.debug("Deleting artifact {}", artifactId);
|
||||||
FileSystem zipDisk = getZipDisk(artifactId);
|
FileSystem zipDisk = getZipDisk(artifactId);
|
||||||
Files.delete(getZipDiskPath(artifactId, zipDisk));
|
Files.delete(getZipDiskPath(artifactId, zipDisk));
|
||||||
|
@ -115,8 +111,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
public void deleteFling(UUID flingId) throws IOException {
|
||||||
public void deleteFling(UUID flingId) {
|
|
||||||
URI zipDiskUri = resolveFlingUri(flingId);
|
URI zipDiskUri = resolveFlingUri(flingId);
|
||||||
|
|
||||||
log.debug("Closing zip disk at {}", zipDiskUri);
|
log.debug("Closing zip disk at {}", zipDiskUri);
|
||||||
|
@ -135,8 +130,9 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
|
||||||
log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath);
|
log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath);
|
||||||
Files.delete(zipDiskPath);
|
Files.delete(zipDiskPath);
|
||||||
|
|
||||||
|
artifactRepository.findAllByFlingId(flingId).forEach(ar -> ar.setArchived(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setArchived(UUID artifactId, boolean archived) {
|
private void setArchived(UUID artifactId, boolean archived) {
|
||||||
|
@ -168,7 +164,7 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
URI uri = resolveArtifactUri(artifactId);
|
URI uri = resolveArtifactUri(artifactId);
|
||||||
log.debug("Looking for zip disk at uri {}", uri);
|
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
|
// being opened
|
||||||
synchronized (filesystems) {
|
synchronized (filesystems) {
|
||||||
if (!filesystems.containsKey(uri)) {
|
if (!filesystems.containsKey(uri)) {
|
||||||
|
@ -214,4 +210,9 @@ public class FileSystemArchive implements ArchiveService {
|
||||||
public void setArchivePath(String archivePath) {
|
public void setArchivePath(String archivePath) {
|
||||||
this.archivePath = Paths.get(archivePath);
|
this.archivePath = Paths.get(archivePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setArchivePath(Path archivePath) {
|
||||||
|
this.archivePath = archivePath;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,37 @@
|
||||||
package net.friedl.fling.controller;
|
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.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
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.ComponentScan.Filter;
|
||||||
import org.springframework.context.annotation.FilterType;
|
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,
|
@WebMvcTest(controllers = ArtifactController.class,
|
||||||
// do auto-configure security
|
// do auto-configure security
|
||||||
|
@ -11,34 +39,102 @@ import org.springframework.context.annotation.FilterType;
|
||||||
// do not try to create beans in security
|
// do not try to create beans in security
|
||||||
excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*"))
|
excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*"))
|
||||||
class ArtifactControllerTest {
|
class ArtifactControllerTest {
|
||||||
// @Autowired
|
@Autowired
|
||||||
// private MockMvc mvc;
|
private MockMvc mvc;
|
||||||
//
|
|
||||||
// @MockBean
|
@MockBean
|
||||||
// private ArtifactService artifactService;
|
private ArtifactService artifactService;
|
||||||
//
|
|
||||||
// @Test
|
@MockBean
|
||||||
// public void testGetArtifacts_noArtifacts_empty() throws Exception {
|
private ArchiveService archiveService;
|
||||||
//// Long flingId = 123L;
|
|
||||||
////
|
private static final UUID ARTIFACT_ID = UUID.randomUUID();
|
||||||
//// when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of());
|
|
||||||
////
|
private ArtifactDto artifactDto =
|
||||||
//// mvc.perform(get("/api/artifacts").param("flingId", flingId.toString()))
|
new ArtifactDto(ARTIFACT_ID, Path.of("testArtifact"), Instant.EPOCH, false);
|
||||||
//// .andExpect(jsonPath("$", hasSize(0)));
|
|
||||||
// }
|
@Test
|
||||||
//
|
public void getArtifact_noArtifactWithId_notFound() throws Exception {
|
||||||
// @Test
|
when(artifactService.getById(ARTIFACT_ID)).thenThrow(EntityNotFoundException.class);
|
||||||
// public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception {
|
|
||||||
//// Long flingId = 123L;
|
mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
//// String artifactName = "TEST";
|
.andExpect(status().isNotFound());
|
||||||
////
|
}
|
||||||
//// ArtifactDto artifactDto = new ArtifactDto();
|
|
||||||
//// artifactDto.setName(artifactName);
|
@Test
|
||||||
////
|
public void getArtifacts_ok() throws Exception {
|
||||||
//// when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto));
|
when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto);
|
||||||
////
|
|
||||||
//// mvc.perform(get("/api/artifacts").param("flingId", flingId.toString()))
|
mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
|
||||||
//// .andExpect(jsonPath("$", hasSize(1)))
|
.andExpect(status().isOk())
|
||||||
//// .andExpect(jsonPath("$[0].name", equalTo(artifactName)));
|
.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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FlingEntity>() {
|
||||||
|
@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<ArtifactEntity>() {
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<FlingEntity>() {
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String> 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<Path>() {
|
||||||
|
@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<Path>() {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
artifact1 ok
|
|
@ -0,0 +1 @@
|
||||||
|
artifact2 ok
|
Loading…
Reference in a new issue