Add unit tests
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Armin Friedl 2020-07-05 18:57:19 +02:00
parent 2b18120c0b
commit a66b51e1e5
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
20 changed files with 1045 additions and 206 deletions

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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")

View file

@ -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);
} }

View file

@ -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);
} }
} }

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
} }

View file

@ -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;
}
} }

View file

@ -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));
}
} }

View file

@ -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));
}
}

View file

@ -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());
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1 @@
artifact1 ok

View file

@ -0,0 +1 @@
artifact2 ok