diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index c315043..9dda3b6 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip diff --git a/.travis.yml b/.travis.yml index 0db1bd9..785a1e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,7 @@ language: java jdk: - oraclejdk8 sudo: false -script: ./mvnw clean verify \ No newline at end of file +script: ./mvnw clean verify cobertura:cobertura-integration-test + +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index acba8b1..7c8f470 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,29 @@ # Boxitory -is repository for Vagrant's Virtual Machine boxes, which can manage box versions and provides *Vagrant* compatible http interface. +is repository for Vagrant's Virtual Machine boxes, which can manage box versions and provides *Vagrant* compatible http interface. Boxes are stored on local filesystem. + +Download [Latest release](https://github.com/sparkoo/boxitory/releases/latest) + +For more info how it works, how to configure, ... [See Wiki](https://github.com/sparkoo/boxitory/wiki) ## Build & run -`./mvnw install && java -jar target/boxitory-{version}.jar` +``` +$ ./mvnw install && java -jar target/boxitory-{version}.jar +``` +or +``` +$ ./mvnw spring-boot:run +``` By default, http server will start on port *8083*. #### Build status (travis-ci) -devel [![Build Status](https://travis-ci.org/sparkoo/boxitory.svg?branch=devel)](https://travis-ci.org/sparkoo/boxitory) - -master: [![Build Status](https://travis-ci.org/sparkoo/boxitory.svg?branch=master)](https://travis-ci.org/sparkoo/boxitory) - -## How it works - -*Boxitory* currently implements just filesystem box provider. That requires strict folder structure. +devel [![Build Status](https://travis-ci.org/sparkoo/boxitory.svg?branch=devel)](https://travis-ci.org/sparkoo/boxitory) +[![codecov](https://codecov.io/gh/sparkoo/boxitory/branch/devel/graph/badge.svg)](https://codecov.io/gh/sparkoo/boxitory) -### Box files on filesystem -There must be one home folder for all boxes with subfolders for each box type. Individial box versions must be named `{name}_{version}_{provider}.box`. - -See example below: -``` -$ tree test_repository/ -test_repository/ -├── f25 -│   ├── f25_1_virtualbox.box -│   └── f25_2_virtualbox.box -├── f26 -│   ├── f26_1_virtualbox.box -│   ├── f26_2_virtualbox.box -│   └── f26_3_virtualbox.box -``` - -### Http interface - -Server starts at port *8083* and boxes can be requested on `http://hostname:port/box_name` for example: -``` -$ curl http://localhost:8083/f26 -{ - "name": "f26", - "description": "f26", - "versions": [ - { - "version": "1", - "providers": [ - { - "url": "sftp://my_box_server:/tmp/test_repository/f26/f26_1_virtualbox.box", - "name": "virtualbox" - } - ] - }, - { - "version": "2", - "providers": [ - { - "url": "sftp://my_box_server:/tmp/test_repository/f26/f26_2_virtualbox.box", - "name": "virtualbox" - } - ] - }, - ] -} -``` +master: [![Build Status](https://travis-ci.org/sparkoo/boxitory.svg?branch=master)](https://travis-ci.org/sparkoo/boxitory) -## Configuration -### Options - * `box.home` - * path where to find boxes - * in example above, it will be set to `/tmp/test_repository` - * **default value**: `.` - * `box.prefix` - * prefix for the output json, that is prepend before absolute local path of the box - * do define for example protocol or server, where boxes are placed - * e.g.: `sftp://my_box_server:` - * **default value**: *empty* - * `box.sort_desc` - * boolean value `true|false` - * when default or `false`, boxes are sorted by version in ascending order - * when `true`, boxes are sorted by version in descending order - * default value: `false` - * `box.checksum` - * string value: `disabled|md5|sha1|sha256` - * default value: `disabled` - * when default or `disabled` boxes output json not contains properties `checksumType` and `checksum` - * when `md5|sha1|sha256` boxes output json contains properties `checksumType` and `checksum` with coresponding values -### Advanced Options - * `box.checksum_buffer_size` - * Box file is loaded to this buffer to calculate box checksums - * default value: `1024` - -### How to configuration -Configuration can be provided by `application.properties` file on classpath -``` -# application.properties -box.home=/tmp/test_repository -box.prefix=sftp://my_box_server: -``` -or as command line arguments `java -jar -Dbox.home=/tmp/test_repository target/boxsitory-${version}.jar` diff --git a/application.properties_template b/application.properties_template index 21b68a0..64087e1 100644 --- a/application.properties_template +++ b/application.properties_template @@ -1,5 +1,9 @@ box.home=/custom/test/repository/path box.host_prefix=sftp://localhost: server.port=8083 -box.checksum=disabled + box.sort_desc=false + +box.checksum=md5 +box.checksum_buffer_size=1024 +box.checksum_persist=true diff --git a/pom.xml b/pom.xml index 3af461b..7a77029 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,38 @@ - + 4.0.0 cz.sparko.boxitory boxitory - 1.2.0 + 1.2.1-SNAPSHOT jar boxitory - Demo project for Spring Boot + Vagrant box repository + https://github.com/sparkoo/boxitory + + + sparkoo + https://github.com/sparkoo + + + + + D3ryk + https://github.com/D3ryk + + + + sparkoo + https://github.com/sparkoo + org.springframework.boot spring-boot-starter-parent 1.5.3.RELEASE - + @@ -34,8 +52,8 @@ https://github.com/sparkoo/boxitory scm:git:git://github.com/sparkoo/boxitory.git scm:git:git@github.com:sparkoo/boxitory.git - v1.2.0 - + HEAD + UTF-8 @@ -94,8 +112,48 @@ maven-release-plugin 2.5.3 + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + html + xml + + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20 + + e2e + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18.1 + + e2e + + **/*Test.java + + + + + + integration-test + verify + + + + - - diff --git a/src/main/java/cz/sparko/boxitory/App.java b/src/main/java/cz/sparko/boxitory/App.java index db4acec..d21744d 100644 --- a/src/main/java/cz/sparko/boxitory/App.java +++ b/src/main/java/cz/sparko/boxitory/App.java @@ -3,8 +3,13 @@ import cz.sparko.boxitory.conf.AppProperties; import cz.sparko.boxitory.factory.HashServiceFactory; import cz.sparko.boxitory.service.BoxRepository; -import cz.sparko.boxitory.service.FilesystemBoxRepository; +import cz.sparko.boxitory.service.DescriptionProvider; +import cz.sparko.boxitory.service.filesystem.FilesystemBoxRepository; +import cz.sparko.boxitory.service.filesystem.FilesystemDescriptionProvider; +import cz.sparko.boxitory.service.filesystem.FilesystemHashStore; import cz.sparko.boxitory.service.HashService; +import cz.sparko.boxitory.service.HashStore; +import cz.sparko.boxitory.service.noop.NoopHashStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -21,8 +26,31 @@ public static void main(String[] args) { @Bean @Autowired - public BoxRepository boxRepository(AppProperties appProperties) throws NoSuchAlgorithmException { - HashService hashService = HashServiceFactory.createHashService(appProperties); - return new FilesystemBoxRepository(appProperties, hashService); + public BoxRepository boxRepository(AppProperties appProperties, + HashService hashService, + DescriptionProvider descriptionProvider) { + return new FilesystemBoxRepository(appProperties, hashService, descriptionProvider); + } + + @Bean + @Autowired + public DescriptionProvider descriptionProvider(AppProperties appProperties) { + return new FilesystemDescriptionProvider(appProperties.getHome()); + } + + @Bean + @Autowired + public HashService hashService(AppProperties appProperties, HashStore hashStore) throws NoSuchAlgorithmException { + return HashServiceFactory.createHashService(appProperties, hashStore); + } + + @Bean + @Autowired + public HashStore hashStore(AppProperties appProperties) { + if (appProperties.isChecksum_persist()) { + return new FilesystemHashStore(); + } else { + return new NoopHashStore(); + } } } diff --git a/src/main/java/cz/sparko/boxitory/conf/AppProperties.java b/src/main/java/cz/sparko/boxitory/conf/AppProperties.java index 5e0f01d..e1c0653 100644 --- a/src/main/java/cz/sparko/boxitory/conf/AppProperties.java +++ b/src/main/java/cz/sparko/boxitory/conf/AppProperties.java @@ -1,5 +1,6 @@ package cz.sparko.boxitory.conf; +import cz.sparko.boxitory.service.HashService.HashAlgorithm; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -8,8 +9,10 @@ public class AppProperties { private String home = "."; private String host_prefix = ""; - private String checksum = "disabled"; private boolean sort_desc = false; + + private HashAlgorithm checksum = HashAlgorithm.DISABLED; + private boolean checksum_persist = true; private int checksum_buffer_size = 1024; public String getHome() { @@ -24,7 +27,7 @@ public boolean isSort_desc() { return sort_desc; } - public String getChecksum() { + public HashAlgorithm getChecksum() { return checksum; } @@ -32,6 +35,10 @@ public int getChecksum_buffer_size() { return checksum_buffer_size; } + public boolean isChecksum_persist() { + return checksum_persist; + } + public void setSort_desc(boolean sort_desc) { this.sort_desc = sort_desc; } @@ -44,11 +51,15 @@ public void setHost_prefix(String host_prefix) { this.host_prefix = host_prefix; } - public void setChecksum(String checksum) { + public void setChecksum(HashAlgorithm checksum) { this.checksum = checksum; } public void setChecksum_buffer_size(int checksum_buffer_size) { this.checksum_buffer_size = checksum_buffer_size; } + + public void setChecksum_persist(boolean checksum_persist) { + this.checksum_persist = checksum_persist; + } } diff --git a/src/main/java/cz/sparko/boxitory/conf/NotFoundException.java b/src/main/java/cz/sparko/boxitory/conf/NotFoundException.java index 0370e58..7398039 100644 --- a/src/main/java/cz/sparko/boxitory/conf/NotFoundException.java +++ b/src/main/java/cz/sparko/boxitory/conf/NotFoundException.java @@ -1,11 +1,14 @@ package cz.sparko.boxitory.conf; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.client.HttpClientErrorException; -@ResponseStatus(value = HttpStatus.NOT_FOUND) -public class NotFoundException extends RuntimeException { +public class NotFoundException extends HttpClientErrorException { public NotFoundException(String message) { - super(message); + super(HttpStatus.NOT_FOUND, message); + } + + public static NotFoundException boxNotFound(String boxName) { + return new NotFoundException("box [" + boxName + "] does not exist"); } } diff --git a/src/main/java/cz/sparko/boxitory/controller/BoxController.java b/src/main/java/cz/sparko/boxitory/controller/BoxController.java index 977a939..e3a44c5 100644 --- a/src/main/java/cz/sparko/boxitory/controller/BoxController.java +++ b/src/main/java/cz/sparko/boxitory/controller/BoxController.java @@ -2,11 +2,19 @@ import cz.sparko.boxitory.conf.NotFoundException; import cz.sparko.boxitory.domain.Box; +import cz.sparko.boxitory.domain.BoxVersion; import cz.sparko.boxitory.service.BoxRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; @Controller public class BoxController { @@ -21,7 +29,7 @@ public BoxController(BoxRepository boxRepository) { @ResponseBody public Box box(@PathVariable String boxName) { return boxRepository.getBox(boxName) - .orElseThrow(() -> new NotFoundException("box [" + boxName + "] does not exist")); + .orElseThrow(() -> NotFoundException.boxNotFound(boxName)); } @RequestMapping(value = "/", method = RequestMethod.GET) @@ -29,4 +37,27 @@ public String index(Model model) { model.addAttribute("boxes", boxRepository.getBoxes()); return "index"; } + + @RequestMapping(value = "/{boxName}/latestVersion", method = RequestMethod.GET) + @ResponseBody + public String latestBoxVersion(@PathVariable String boxName) { + return boxRepository.getBox(boxName) + .orElseThrow(() -> NotFoundException.boxNotFound(boxName)) + .getVersions().stream() + .sorted(BoxVersion.VERSION_COMPARATOR.reversed()) + .findFirst() + .map(BoxVersion::getVersion) + .orElseThrow(() -> NotFoundException.boxNotFound(boxName)); + } + + @ExceptionHandler + public ResponseEntity handleException(Exception e) { + final HttpStatus status; + if (e instanceof NotFoundException) { + status = HttpStatus.NOT_FOUND; + } else { + status = HttpStatus.INTERNAL_SERVER_ERROR; + } + return new ResponseEntity<>(e.getMessage(), status); + } } diff --git a/src/main/java/cz/sparko/boxitory/domain/BoxProvider.java b/src/main/java/cz/sparko/boxitory/domain/BoxProvider.java index 1901716..16a3f3f 100644 --- a/src/main/java/cz/sparko/boxitory/domain/BoxProvider.java +++ b/src/main/java/cz/sparko/boxitory/domain/BoxProvider.java @@ -1,6 +1,8 @@ package cz.sparko.boxitory.domain; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import java.util.Objects; @@ -8,6 +10,7 @@ public class BoxProvider { private final String url; private final String name; + @JsonProperty("checksum_type") private final String checksumType; private final String checksum; diff --git a/src/main/java/cz/sparko/boxitory/domain/BoxVersion.java b/src/main/java/cz/sparko/boxitory/domain/BoxVersion.java index f32bdc4..dff9d34 100644 --- a/src/main/java/cz/sparko/boxitory/domain/BoxVersion.java +++ b/src/main/java/cz/sparko/boxitory/domain/BoxVersion.java @@ -1,14 +1,23 @@ package cz.sparko.boxitory.domain; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Comparator; import java.util.List; import java.util.Objects; +@JsonInclude(JsonInclude.Include.NON_NULL) public class BoxVersion { + public static final Comparator VERSION_COMPARATOR = + Comparator.comparingInt(o -> Integer.parseInt(o.getVersion())); + private final String version; + private final String description; private final List providers; - public BoxVersion(String version, List providers) { + public BoxVersion(String version, String description, List providers) { this.version = version; + this.description = description; this.providers = providers; } @@ -16,6 +25,7 @@ public BoxVersion(String version, List providers) { public String toString() { return "BoxVersion{" + "version='" + version + '\'' + + ", description='" + description + '\'' + ", providers=" + providers + '}'; } @@ -24,14 +34,15 @@ public String toString() { public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } - BoxVersion thatVersion = (BoxVersion) o; - return Objects.equals(this.version, thatVersion.version) && - providers.containsAll(thatVersion.getProviders()) && thatVersion.getProviders().containsAll(providers); + BoxVersion that = (BoxVersion) o; + return Objects.equals(version, that.version) && + Objects.equals(description, that.description) && + providers.containsAll(that.getProviders()) && that.getProviders().containsAll(providers); } @Override public int hashCode() { - return Objects.hash(version, providers); + return Objects.hash(version, description, providers); } public String getVersion() { @@ -41,4 +52,8 @@ public String getVersion() { public List getProviders() { return providers; } + + public String getDescription() { + return description; + } } diff --git a/src/main/java/cz/sparko/boxitory/factory/HashServiceFactory.java b/src/main/java/cz/sparko/boxitory/factory/HashServiceFactory.java index a1e7e84..2220bc9 100644 --- a/src/main/java/cz/sparko/boxitory/factory/HashServiceFactory.java +++ b/src/main/java/cz/sparko/boxitory/factory/HashServiceFactory.java @@ -1,31 +1,25 @@ package cz.sparko.boxitory.factory; import cz.sparko.boxitory.conf.AppProperties; -import cz.sparko.boxitory.service.FilesystemDigestHashService; -import cz.sparko.boxitory.service.NoopHashService; +import cz.sparko.boxitory.service.filesystem.FilesystemDigestHashService; +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.noop.NoopHashService; import cz.sparko.boxitory.service.HashService; +import cz.sparko.boxitory.service.HashStore; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashServiceFactory { - public static HashService createHashService(AppProperties appProperties) throws NoSuchAlgorithmException { - String algorithm = appProperties.getChecksum().toUpperCase(); - - switch (algorithm) { - case "MD5": - return new FilesystemDigestHashService(MessageDigest.getInstance(algorithm), appProperties); - case "SHA1": - return new FilesystemDigestHashService(MessageDigest.getInstance("SHA-1"), appProperties); - case "SHA256": - return new FilesystemDigestHashService(MessageDigest.getInstance("SHA-256"), appProperties); - case "DISABLED": - return new NoopHashService(); - default: - throw new IllegalArgumentException( - "Configured checksum type (box.checksum=" + algorithm + ") is not supported" - ); + public static HashService createHashService(AppProperties appProperties, HashStore hashStore) throws + NoSuchAlgorithmException { + HashAlgorithm algorithm = appProperties.getChecksum(); + if (algorithm == HashAlgorithm.DISABLED) { + return new NoopHashService(); + } else { + return new FilesystemDigestHashService(MessageDigest.getInstance(algorithm.getMessageDigestName()), + appProperties.getChecksum(), appProperties.getChecksum_buffer_size(), hashStore); } } } diff --git a/src/main/java/cz/sparko/boxitory/service/DescriptionProvider.java b/src/main/java/cz/sparko/boxitory/service/DescriptionProvider.java new file mode 100644 index 0000000..6563cdd --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/DescriptionProvider.java @@ -0,0 +1,21 @@ +package cz.sparko.boxitory.service; + +import java.util.Optional; + +/** + * Provides descriptions for box's versions. Implementation is not responsible for storing or handling descriptions + * in any way. It just takes one by given parameters from underlaying storage and provides it to caller. + */ +public interface DescriptionProvider { + /** + * Implementation provides description for given box's version, when available in it's storage. + * + * @param boxName name of the box of which we're requesting description + * @param version particular box version of which we're requesting description + * @return {@link Optional} of description of particular box version. When not found, {@link Optional#empty()} + * returned. + * @throws NullPointerException when provided boxName or version is null + * @throws IllegalArgumentException when provided boxName or version is empty + */ + Optional getDescription(String boxName, String version); +} diff --git a/src/main/java/cz/sparko/boxitory/service/FilesystemDigestHashService.java b/src/main/java/cz/sparko/boxitory/service/FilesystemDigestHashService.java deleted file mode 100644 index ebcfb04..0000000 --- a/src/main/java/cz/sparko/boxitory/service/FilesystemDigestHashService.java +++ /dev/null @@ -1,75 +0,0 @@ -package cz.sparko.boxitory.service; - -import cz.sparko.boxitory.conf.AppProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.xml.bind.DatatypeConverter; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.security.MessageDigest; -import java.util.Objects; - -public class FilesystemDigestHashService implements HashService { - - private static final Logger LOG = LoggerFactory.getLogger(FilesystemDigestHashService.class); - private MessageDigest messageDigest; - private int streamBufferLength; - - public FilesystemDigestHashService(MessageDigest messageDigest, AppProperties appProperties) { - this.messageDigest = messageDigest; - streamBufferLength = appProperties.getChecksum_buffer_size(); - } - - @Override - public String getHashType() { - return messageDigest.getAlgorithm().replaceAll("-", "").toLowerCase(); - } - - @Override - public String getChecksum(String string) { - try (InputStream boxDataStream = Files.newInputStream(new File(string).toPath())) { - LOG.trace("buffering box data (buffer size [{}]b) ...", streamBufferLength); - final byte[] buffer = new byte[streamBufferLength]; - int read = boxDataStream.read(buffer, 0, streamBufferLength); - - while (read > -1) { - messageDigest.update(buffer, 0, read); - read = boxDataStream.read(buffer, 0, streamBufferLength); - } - } catch (IOException e) { - LOG.error("Error during processing file [{}], message: [{}]", string, e.getMessage()); - throw new RuntimeException( - "Error while getting checksum for file " + string + " reason: " + e.getMessage(), e - ); - } - - return getHash(messageDigest.digest()); - } - - private String getHash(byte[] diggestBytes) { - return DatatypeConverter.printHexBinary(diggestBytes).toLowerCase(); - } - - @Override - public String toString() { - return "FilesystemDigestHashService{" + - "messageDigest=" + messageDigest + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } - FilesystemDigestHashService that = (FilesystemDigestHashService) o; - return messageDigest.getAlgorithm().equals(that.messageDigest.getAlgorithm()); - } - - @Override - public int hashCode() { - return Objects.hash(messageDigest); - } -} diff --git a/src/main/java/cz/sparko/boxitory/service/HashService.java b/src/main/java/cz/sparko/boxitory/service/HashService.java index 75482a1..f8fdb00 100644 --- a/src/main/java/cz/sparko/boxitory/service/HashService.java +++ b/src/main/java/cz/sparko/boxitory/service/HashService.java @@ -1,7 +1,35 @@ package cz.sparko.boxitory.service; - public interface HashService { String getHashType(); - String getChecksum(String string); + String getChecksum(String box); + + enum HashAlgorithm { + MD5("MD5", ".md5", "md5"), + SHA1("SHA-1", ".sha1", "sha1"), + SHA256("SHA-256", ".sha256", "sha256"), + DISABLED("", ".noop", ""); + + private final String messageDigestName; + private final String fileExtension; + private final String vagrantInterfaceName; + + HashAlgorithm(String messageDigestName, String fileExtension, String vagrantInterfaceName) { + this.messageDigestName = messageDigestName; + this.fileExtension = fileExtension; + this.vagrantInterfaceName = vagrantInterfaceName; + } + + public String getMessageDigestName() { + return messageDigestName; + } + + public String getFileExtension() { + return fileExtension; + } + + public String getVagrantInterfaceName() { + return vagrantInterfaceName; + } + } } diff --git a/src/main/java/cz/sparko/boxitory/service/HashStore.java b/src/main/java/cz/sparko/boxitory/service/HashStore.java new file mode 100644 index 0000000..fc38049 --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/HashStore.java @@ -0,0 +1,29 @@ +package cz.sparko.boxitory.service; + +import cz.sparko.boxitory.service.HashService.HashAlgorithm; + +import java.util.Optional; + +/** + * Responsible for persisting calculated hash to underlying store. + */ +public interface HashStore { + /** + * Persist hash to underlying storage. It's up to implementation whether replace previous stored value, ignore, + * or throw {@link RuntimeException}. + * + * @param box path to box. May vary depending on implementation. + * @param hash calculated hash + * @param algorithm algorithm of the hash + */ + void persist(String box, String hash, HashAlgorithm algorithm); + + /** + * Load previously persisted hash for given {@code box}. + * + * @param box path to box. May vary depending on implementation. + * @param algorithm algorithm of the hash + * @return hash for {@code box} when found, {@link Optional#empty()} otherwise + */ + Optional loadHash(String box, HashAlgorithm algorithm); +} diff --git a/src/main/java/cz/sparko/boxitory/service/FilesystemBoxRepository.java b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemBoxRepository.java similarity index 75% rename from src/main/java/cz/sparko/boxitory/service/FilesystemBoxRepository.java rename to src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemBoxRepository.java index 95d1b67..3bd9f36 100644 --- a/src/main/java/cz/sparko/boxitory/service/FilesystemBoxRepository.java +++ b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemBoxRepository.java @@ -1,21 +1,26 @@ -package cz.sparko.boxitory.service; +package cz.sparko.boxitory.service.filesystem; import cz.sparko.boxitory.conf.AppProperties; import cz.sparko.boxitory.domain.Box; import cz.sparko.boxitory.domain.BoxProvider; import cz.sparko.boxitory.domain.BoxVersion; +import cz.sparko.boxitory.service.BoxRepository; +import cz.sparko.boxitory.service.DescriptionProvider; +import cz.sparko.boxitory.service.HashService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cz.sparko.boxitory.domain.BoxVersion.VERSION_COMPARATOR; public class FilesystemBoxRepository implements BoxRepository { private static final Logger LOG = LoggerFactory.getLogger(FilesystemBoxRepository.class); @@ -25,17 +30,22 @@ public class FilesystemBoxRepository implements BoxRepository { private final HashService hashService; private final boolean sortDesc; - public FilesystemBoxRepository(AppProperties appProperties, HashService hashService) { + private final DescriptionProvider descriptionProvider; + + public FilesystemBoxRepository(AppProperties appProperties, + HashService hashService, + DescriptionProvider descriptionProvider) { this.boxHome = new File(appProperties.getHome()); this.hostPrefix = appProperties.getHost_prefix(); this.sortDesc = appProperties.isSort_desc(); this.hashService = hashService; + this.descriptionProvider = descriptionProvider; LOG.info("setting BOX_HOME as [{}] and HOST_PREFIX as [{}]", boxHome.getAbsolutePath(), hostPrefix); } @Override public List getBoxes() { - return Arrays.stream(boxHome.listFiles(File::isDirectory)) + return listPotencialBoxDirs() .filter(this::containsValidBoxFile) .map(File::getName) .sorted() @@ -48,7 +58,7 @@ public Optional getBox(String boxName) { getBoxDir(boxName) .ifPresent(d -> groupedBoxFiles.putAll(groupBoxFilesByVersion(d))); - List boxVersions = createBoxVersionsFromGroupedFiles(groupedBoxFiles); + List boxVersions = createBoxVersionsFromGroupedFiles(groupedBoxFiles, boxName); if (boxVersions.isEmpty()) { LOG.debug("no box versions found for [{}]", boxName); return Optional.empty(); @@ -59,16 +69,19 @@ public Optional getBox(String boxName) { } private Optional getBoxDir(String boxName) { - File[] boxesHomeFiles = boxHome.listFiles(); - if (boxesHomeFiles == null) { - throw new IllegalStateException("[" + boxHome.getAbsolutePath() + "] is not a valid folder"); - } - return Arrays.stream(boxesHomeFiles) + return listPotencialBoxDirs() .filter(File::isDirectory) .filter(f -> f.getName().equals(boxName)) .findFirst(); } + private Stream listPotencialBoxDirs() { + File[] potencialBoxDirs = Optional.ofNullable(boxHome.listFiles(File::isDirectory)) + .orElseThrow(() -> new IllegalStateException( + "Repository directory [" + boxHome.getAbsolutePath() + "] is not a valid directory.")); + return Arrays.stream(potencialBoxDirs); + } + private Map> groupBoxFilesByVersion(File boxDir) { File[] boxFiles = Optional.ofNullable(boxDir.listFiles()) .orElse(new File[0]); @@ -98,23 +111,23 @@ private String getBoxVersionFromFileName(File file) { return parsedFilename.get(1); } - private List createBoxVersionsFromGroupedFiles(Map> groupedFiles) { + private List createBoxVersionsFromGroupedFiles(Map> groupedFiles, String boxName) { List boxVersions = new ArrayList<>(); groupedFiles.forEach( - (key, value) -> boxVersions.add(createBoxVersion(key, value)) + (key, value) -> boxVersions.add(createBoxVersion(key, value, boxName)) ); - Comparator versionComparator = Comparator.comparingInt(o -> Integer.parseInt(o.getVersion())); if (sortDesc) { - boxVersions.sort(versionComparator.reversed()); + boxVersions.sort(VERSION_COMPARATOR.reversed()); } else { - boxVersions.sort(versionComparator); + boxVersions.sort(VERSION_COMPARATOR); } return boxVersions; } - private BoxVersion createBoxVersion(String version, List fileList) { + private BoxVersion createBoxVersion(String version, List fileList, String boxName) { return new BoxVersion( version, + descriptionProvider.getDescription(boxName, version).orElse(null), fileList.stream().map(this::createBoxProviderFromFile).collect(Collectors.toList()) ); } @@ -137,6 +150,6 @@ private BoxProvider createBoxProviderFromFile(File file) { private boolean containsValidBoxFile(File file) { File[] files = file.listFiles(this::validateFilename); - return files.length > 0; + return files != null && files.length > 0; } } diff --git a/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDescriptionProvider.java b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDescriptionProvider.java new file mode 100644 index 0000000..e9b0ef2 --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDescriptionProvider.java @@ -0,0 +1,102 @@ +package cz.sparko.boxitory.service.filesystem; + +import cz.sparko.boxitory.service.DescriptionProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Objects; +import java.util.Optional; + +/** + * This implementation of {@link DescriptionProvider} uses filesystem as storage. Descriptions must be stored in file + * {@link FilesystemDescriptionProvider#DESCRIPTIONS_FILE} in CSV format with + * {@link FilesystemDescriptionProvider#SEPARATOR} as separator, in box folder beside {@code .box} files. + */ +public class FilesystemDescriptionProvider implements DescriptionProvider { + private static final Logger LOG = LoggerFactory.getLogger(FilesystemDescriptionProvider.class); + + public static final String DESCRIPTIONS_FILE = "descriptions.csv"; + private static final String SEPARATOR = ";;;"; + + private final File boxHome; + + public FilesystemDescriptionProvider(File boxHome) { + this.boxHome = boxHome; + } + + public FilesystemDescriptionProvider(String boxHome) { + this.boxHome = new File(boxHome); + } + + /** + * {@link FilesystemDescriptionProvider} makes best effort to get description for given box's version from file + * {@link FilesystemDescriptionProvider#DESCRIPTIONS_FILE}. That means that when it finds just one valid line in + * {@link FilesystemDescriptionProvider#DESCRIPTIONS_FILE} that matches given parameters, it return it. It does + * not do any validation of the rest of the file. + *

+ * When multiple descriptions for one version found, it returns the latest one. With this behavior, we can simply + * append to the {@link FilesystemDescriptionProvider#DESCRIPTIONS_FILE} and don't care about past descriptions. + */ + @Override + public Optional getDescription(String boxName, String version) { + validateArgs(boxName, version); + + File descriptionFile = new File(boxHome, File.separator + boxName + File.separator + DESCRIPTIONS_FILE); + if (!descriptionFile.exists()) { + LOG.trace("Descriptions file [{}] does not exist.", DESCRIPTIONS_FILE); + return Optional.empty(); + } + try { + Optional foundDescription = Files.readAllLines(descriptionFile.toPath()).stream() + .map(this::parseLine) + .filter(Objects::nonNull) + .filter(parsedLine -> version.equals(parsedLine.version)) + .reduce((a, b) -> b); // get last object + + if (foundDescription.isPresent()) { + String description = foundDescription.get().description; + LOG.debug("Description [{}] found for box [{}] version [{}]", description, boxName, version); + return Optional.of(description); + } + } catch (IOException e) { + LOG.error("Error when parsing description file. Please check whether [{}] is in valid format.", + DESCRIPTIONS_FILE, e); + } + LOG.debug("No description found for box [{}] version [{}]", boxName, version); + return Optional.empty(); + } + + private void validateArgs(String boxName, String version) { + if (boxName == null) { + throw new NullPointerException("[boxName] must not be null nor empty"); + } else if (boxName.isEmpty()) { + throw new IllegalArgumentException("[boxName] must not be null nor empty"); + } + if (version == null) { + throw new NullPointerException("[boxName] must not be null nor empty"); + } else if (version.isEmpty()) { + throw new IllegalArgumentException("[boxName] must not be null nor empty"); + } + } + + private DescriptionLine parseLine(String line) { + String[] splittedLine = line.split(SEPARATOR); + if (splittedLine.length != 2) { + return null; + } + return new DescriptionLine(splittedLine[0], splittedLine[1]); + } + + private static class DescriptionLine { + private final String version; + private final String description; + + DescriptionLine(String version, String description) { + this.version = version; + this.description = description; + } + } +} diff --git a/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDigestHashService.java b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDigestHashService.java new file mode 100644 index 0000000..4d7a860 --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemDigestHashService.java @@ -0,0 +1,128 @@ +package cz.sparko.boxitory.service.filesystem; + +import cz.sparko.boxitory.service.HashService; +import cz.sparko.boxitory.service.HashStore; +import cz.sparko.boxitory.service.noop.NoopHashStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.bind.DatatypeConverter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Objects; + +/** + * This implementation of {@link HashService} calculates checksums from files on filesystem using {@link MessageDigest} + */ +public class FilesystemDigestHashService implements HashService { + private static final Logger LOG = LoggerFactory.getLogger(FilesystemDigestHashService.class); + + private static final int DEFAULT_STREAM_BUFFER_LENGTH = 1024; + + private final MessageDigest messageDigest; + private final int streamBufferLength; + private final HashStore hashStore; + private final HashAlgorithm hashAlgorithm; + + /** + * @param messageDigest instance of {@link MessageDigest} which is used to calculate hashes + * @param hashAlgorithm algorithm used to calculate hashes + * @param streamBufferLength buffer used when calculating hashes + * @param hashStore store used to persist already calculated hashes + */ + public FilesystemDigestHashService(MessageDigest messageDigest, HashAlgorithm hashAlgorithm, + int streamBufferLength, HashStore hashStore) { + this.hashAlgorithm = hashAlgorithm; + this.messageDigest = messageDigest; + this.streamBufferLength = streamBufferLength; + this.hashStore = hashStore; + } + + /** + * See {@link FilesystemDigestHashService#FilesystemDigestHashService(MessageDigest, HashAlgorithm, int, HashStore)} + *

+ * Uses {@link NoopHashStore} as store. + */ + public FilesystemDigestHashService(MessageDigest messageDigest, HashAlgorithm hashAlgorithm, + int streamBufferLength) { + this(messageDigest, hashAlgorithm, streamBufferLength, new NoopHashStore()); + } + + /** + * See {@link FilesystemDigestHashService#FilesystemDigestHashService(MessageDigest, HashAlgorithm, int, HashStore)} + *

+ * Uses {@link NoopHashStore} as store. + *
+ * Uses {@link FilesystemDigestHashService#DEFAULT_STREAM_BUFFER_LENGTH} as {@code streamBufferLength}. + */ + public FilesystemDigestHashService(MessageDigest messageDigest, HashAlgorithm hashAlgorithm) { + this(messageDigest, hashAlgorithm, DEFAULT_STREAM_BUFFER_LENGTH, new NoopHashStore()); + } + + /** + * See {@link FilesystemDigestHashService#FilesystemDigestHashService(MessageDigest, HashAlgorithm, int, HashStore)} + *

+ * Uses {@link FilesystemDigestHashService#DEFAULT_STREAM_BUFFER_LENGTH} as {@code streamBufferLength}. + */ + public FilesystemDigestHashService(MessageDigest messageDigest, HashAlgorithm hashAlgorithm, HashStore hashStore) { + this(messageDigest, hashAlgorithm, DEFAULT_STREAM_BUFFER_LENGTH, hashStore); + } + + @Override + public String getHashType() { + return hashAlgorithm.getVagrantInterfaceName(); + } + + @Override + public String getChecksum(String box) { + final String hash = hashStore.loadHash(box, hashAlgorithm) + .orElseGet(() -> calculateHash(box)); + hashStore.persist(box, hash, hashAlgorithm); + return hash; + } + + private String calculateHash(String box) { + LOG.debug("calculating [{}] hash for box [{}]", hashAlgorithm.name(), box); + try (InputStream boxDataStream = Files.newInputStream(new File(box).toPath()); + InputStream digestInputStream = new DigestInputStream(boxDataStream, messageDigest)) { + LOG.trace("buffering box data (buffer size [{}]b) ...", streamBufferLength); + final byte[] buffer = new byte[streamBufferLength]; + //noinspection StatementWithEmptyBody + while (digestInputStream.read(buffer) > 0) ; + } catch (IOException e) { + LOG.error("Error during processing file [{}], message: [{}]", box, e.getMessage()); + throw new RuntimeException( + "Error while getting checksum for file " + box + " reason: " + e.getMessage(), e); + } + + return getHash(messageDigest.digest()); + } + + private String getHash(byte[] digestBytes) { + return DatatypeConverter.printHexBinary(digestBytes).toLowerCase(); + } + + @Override + public String toString() { + return "FilesystemDigestHashService{" + + "messageDigest=" + messageDigest + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + FilesystemDigestHashService that = (FilesystemDigestHashService) o; + return messageDigest.getAlgorithm().equals(that.messageDigest.getAlgorithm()); + } + + @Override + public int hashCode() { + return Objects.hash(messageDigest); + } +} diff --git a/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemHashStore.java b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemHashStore.java new file mode 100644 index 0000000..383cff4 --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/filesystem/FilesystemHashStore.java @@ -0,0 +1,178 @@ +package cz.sparko.boxitory.service.filesystem; + +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.HashStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Optional; + +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.DISABLED; + +/** + * Stores/reads calculated checksums on/from filesystem. Hash file is named + * {@code {box_filename}.{hash_algorithm_extension}} and has one-line content + *

{@code {calculated_hash}  {box_filename}}
+ * Checksum files can be created manually, but must have proper format. + */ +public class FilesystemHashStore implements HashStore { + private static final Logger LOG = LoggerFactory.getLogger(FilesystemHashStore.class); + + /** + * Store checksum on filesystem, beside box file with appended extension derived from + * {@link HashAlgorithm}. When already exists, original file is not replaced. + * When file can't be created for any reason (e.g. write permissions), error is logged but no exception raised. + * + * @param box absolute filesystem path to the box + * @param hash calculated hash + * @param algorithm algorithm of hash. Must not be {@link HashAlgorithm#DISABLED} + * @throws IllegalStateException when file provided by {@code box} does not exists + * @throws IllegalStateException when {@code algorithm} is {@link HashAlgorithm#DISABLED} + */ + @Override + public void persist(String box, String hash, HashAlgorithm algorithm) { + File boxFile = getBoxFile(box); + + if (algorithm == DISABLED) { + LOG.error("No hash algorithm [{}]. Nothing to persist.", algorithm); + throw new IllegalStateException("Trying to persist hash with [" + DISABLED + "] algorithm set. " + + "This is not a valid state."); + } + + try { + String boxHashFilename = box + algorithm.getFileExtension(); + File hashFile = new File(boxHashFilename); + if (hashFile.exists()) { + LOG.trace("Hash file [{}] already exist. Not replacing!", boxHashFilename); + return; + } + boolean fileCreated = hashFile.createNewFile(); + if (fileCreated) { + try (FileWriter writer = new FileWriter(hashFile)) { + writer.write(ChecksumFileHandler.createValidChecksumFileContent(hash, boxFile)); + LOG.debug("Storing hash file [{}]", hashFile.getName()); + } + } else { + LOG.debug("Hash file [{}] was not created. Dropping."); + } + } catch (IOException e) { + LOG.error("Something went wrong when persisting hash file.", e); + } + } + + /** + * Reads checksum from filesystem, from file beside box file with extension derived from {@link HashAlgorithm}. + * + * @param box absolute filesystem path to the box + * @param algorithm algorithm of hash + * @return hash when file found, {@link Optional#empty()} otherwise + * @throws IllegalStateException when file provided by {@code box} does not exists + */ + @Override + public Optional loadHash(String box, HashAlgorithm algorithm) { + validateBoxFile(getBoxFile(box)); + + final Optional hash; + + File boxHashFile = new File(box + algorithm.getFileExtension()); + if (boxHashFile.exists() && boxHashFile.isFile()) { + LOG.trace("Found hash file [{}] for box version [{}]", boxHashFile.getAbsolutePath(), box); + try { + hash = Optional.of(ChecksumFileHandler.readHashFromChecksumFile(boxHashFile)); + LOG.debug("Hash [{}] loaded from file [{}] for box [{}]", hash, boxHashFile.getAbsolutePath(), box); + } catch (IllegalStateException ise) { + LOG.error("Checksum file [{}] has probably wrong format.", boxHashFile, ise); + return Optional.empty(); + } catch (IOException e) { + LOG.error("Something went wrong when reading hash file.", e); + return Optional.empty(); + } + } else { + hash = Optional.empty(); + } + return hash; + } + + /** + * Get {@link File} for provided {@code box} + * + * @param box path to the box file + * @throws IllegalStateException when box file does not exist + */ + private File getBoxFile(String box) { + File boxFile = new File(box); + validateBoxFile(boxFile); + return boxFile; + } + + /** + * Throw {@link IllegalStateException} when box file does not exists. + * + * @param boxFile file to check + */ + private void validateBoxFile(File boxFile) { + if (!boxFile.exists() || !boxFile.isFile()) { + throw new IllegalStateException("box [" + boxFile.getName() + "] does not exist!"); + } + } + + /** + * Can parse or create valid checksum file content. Valid format of file is one-line + * + *
{@code {calculated_hash}  {box_filename}}
+ *
+ * This class uses {@link ChecksumFileHandler#CHECKSUM_FILE_SEPARATOR} as separator. + */ + private static class ChecksumFileHandler { + private final static String CHECKSUM_FILE_SEPARATOR = " "; + + /** + * Creates valid checksum file content from provided {@code hash} and {@link File} {@code forFile}. + * + * @param hash calculated hash for file + * @param forFile file of calculated hash + * @return valid content of the checksum file + */ + static String createValidChecksumFileContent(String hash, File forFile) { + return hash + CHECKSUM_FILE_SEPARATOR + forFile.getName(); + } + + /** + * Reads hash from given {@link File} {@code checksumFile} + * + * @param checksumFile read hash from this file. Filename must be in format {original_filename}.{checksum_alg} + * @return {@code checksumFile}'s checksum + * @throws IOException when some error when reading the file occurs + * @throws IllegalStateException when checksum file has wrong format + */ + static String readHashFromChecksumFile(File checksumFile) + throws IOException, IllegalStateException { + List hashFileLines = Files.readAllLines(checksumFile.toPath()); + if (hashFileLines.size() != 1) { + throw new IllegalStateException("Checksum file has wrong format!"); + } + + String boxFilename = parseOriginalFilenameFromChecksumFile(checksumFile); + String[] hashSplittedLine = hashFileLines.get(0).split(CHECKSUM_FILE_SEPARATOR); + if (hashSplittedLine.length != 2 || !hashSplittedLine[1].equals(boxFilename)) { + throw new IllegalStateException("Checksum file has wrong format!"); + } + + return hashSplittedLine[0]; + } + + private static String parseOriginalFilenameFromChecksumFile(File checksumFile) throws IllegalStateException { + String checksumFilename = checksumFile.getName(); + int lastDotIndex = checksumFilename.lastIndexOf('.'); + if (lastDotIndex < 0) { + throw new IllegalStateException("Checksum file has wrong name! [" + checksumFilename + "]"); + } + return checksumFilename.substring(0, lastDotIndex); + } + } +} diff --git a/src/main/java/cz/sparko/boxitory/service/noop/NoopDescriptionProvider.java b/src/main/java/cz/sparko/boxitory/service/noop/NoopDescriptionProvider.java new file mode 100644 index 0000000..1141afe --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/noop/NoopDescriptionProvider.java @@ -0,0 +1,12 @@ +package cz.sparko.boxitory.service.noop; + +import cz.sparko.boxitory.service.DescriptionProvider; + +import java.util.Optional; + +public class NoopDescriptionProvider implements DescriptionProvider { + @Override + public Optional getDescription(String boxName, String version) { + return Optional.empty(); + } +} diff --git a/src/main/java/cz/sparko/boxitory/service/NoopHashService.java b/src/main/java/cz/sparko/boxitory/service/noop/NoopHashService.java similarity index 70% rename from src/main/java/cz/sparko/boxitory/service/NoopHashService.java rename to src/main/java/cz/sparko/boxitory/service/noop/NoopHashService.java index f3aeaa5..3a46220 100644 --- a/src/main/java/cz/sparko/boxitory/service/NoopHashService.java +++ b/src/main/java/cz/sparko/boxitory/service/noop/NoopHashService.java @@ -1,6 +1,8 @@ -package cz.sparko.boxitory.service; +package cz.sparko.boxitory.service.noop; +import cz.sparko.boxitory.service.HashService; + public class NoopHashService implements HashService { @Override @@ -9,7 +11,7 @@ public String getHashType() { } @Override - public String getChecksum(String string) { + public String getChecksum(String box) { return null; } diff --git a/src/main/java/cz/sparko/boxitory/service/noop/NoopHashStore.java b/src/main/java/cz/sparko/boxitory/service/noop/NoopHashStore.java new file mode 100644 index 0000000..7bb2ab0 --- /dev/null +++ b/src/main/java/cz/sparko/boxitory/service/noop/NoopHashStore.java @@ -0,0 +1,20 @@ +package cz.sparko.boxitory.service.noop; + +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.HashStore; + +import java.util.Optional; + +/** + * Does nothing. Used when checksum persist is disabled. + */ +public class NoopHashStore implements HashStore { + @Override + public void persist(String boxFilename, String hash, HashAlgorithm algorithm) { + } + + @Override + public Optional loadHash(String box, HashAlgorithm algorithm) { + return Optional.empty(); + } +} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..7e6d23d --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,33 @@ +{ + "properties": [ + { + "name": "box.home", + "type": "java.lang.String", + "description": "Home of Boxitory repository." + }, + { + "name": "box.host_prefix", + "type": "java.lang.String", + "description": "Prefix for box URL used on HTTP API." + }, + { + "name": "box.sort_desc", + "type": "java.lang.Boolean", + "description": "Box sorting order." + }, + { + "name": "box.checksum", + "type": "java.lang.String", + "description": "Checksum algorithm." + }, + { + "name": "box.checksum_persist", + "type": "java.lang.Boolean", + "description": "Persist already calculated hashes." + }, + { + "name": "box.checksum_buffer_size", + "type": "java.lang.Integer", + "description": "Buffer for hash calculations." + } + ] } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 21818de..562385b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,35 @@ +#### BOXITORY #### +# This is one and only configuration file for Boxitory server. +# For more details about configuration, please visit our wiki at https://github.com/sparkoo/boxitory/wiki/Configuration + + +### SERVER ### + +# port where http server is listening +server.port=8083 + +# Absolute or relative path to box repository root. box.home=. + +# Prefix that is exploited on HTTP API and is prepended before box absolute path in url. +# Typically it's protocol + hostname (e.g. 'sftp://username@hostname:' ) box.host_prefix= -box.checksum=disabled -server.port=8083 -box.sort_desc=false \ No newline at end of file + +# Boxes are sorted by versions. This set it ascending or descending order. +box.sort_desc=false + + +### Checksum ### + +# Disable or pick hash algorithm. Supported algorithms are md5, sha1, sha256. +# Valid values are disabled|md5|sha1|sha256 case insensitive. +# WARNING! Calculating checksums is time consuming operation. +# Optimizations can be done with `box.checksum_persist` and `box.checksum_buffer_size`. +box.checksum=DISABLED + +# Prevents recalculating of checksums for same boxes. When enabled, checksum once calculated is persisted beside the box +# and just read next time. +box.checksum_persist=true + +# Buffer used when calculating checksums. You can tune this value and find what best suite your hardware +box.checksum_buffer_size=1024 diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..25d3826 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _ _ _ +| | (_) | ${AnsiColor.YELLOW}${application.formatted-version}${AnsiStyle.NORMAL} +| |__ _____ ___| |_ ___ _ __ _ _ +| '_ \ / _ \ \/ / | __/ _ \| '__| | | | +| |_) | (_) > <| | || (_) | | | |_| | +|_.__/ \___/_/\_\_|\__\___/|_| \__, | + __/ | + |___/ diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index b9cbec6..bc412d8 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/src/test/java/cz/sparko/boxitory/service/FilesystemBoxRepositoryTest.java b/src/test/java/cz/sparko/boxitory/service/FilesystemBoxRepositoryTest.java index 4198b99..e76f335 100644 --- a/src/test/java/cz/sparko/boxitory/service/FilesystemBoxRepositoryTest.java +++ b/src/test/java/cz/sparko/boxitory/service/FilesystemBoxRepositoryTest.java @@ -4,10 +4,14 @@ import cz.sparko.boxitory.domain.Box; import cz.sparko.boxitory.domain.BoxVersion; import cz.sparko.boxitory.domain.BoxProvider; +import cz.sparko.boxitory.service.filesystem.FilesystemBoxRepository; +import cz.sparko.boxitory.service.noop.NoopDescriptionProvider; +import cz.sparko.boxitory.service.noop.NoopHashService; import org.apache.commons.io.FileUtils; import org.springframework.boot.test.context.SpringBootTest; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -27,18 +31,28 @@ public class FilesystemBoxRepositoryTest { private final String TEST_HOME = "target/test_repository"; private final String TEST_BOX_PREFIX = "sftp://my_test_server:"; + private final String VERSION_DESCRIPTION = null; private File testHomeDir; private AppProperties testAppProperties; @BeforeClass public void setUp() throws IOException { + testHomeDir = new File(TEST_HOME); + + createTestFolderStructure(); + } + + @BeforeMethod + public void testSetUp() { testAppProperties = new AppProperties(); testAppProperties.setHome(TEST_HOME); testAppProperties.setHost_prefix(TEST_BOX_PREFIX); - testHomeDir = new File(TEST_HOME); + } - createTestFolderStructure(); + @AfterClass + public void tearDown() throws IOException { + FileUtils.deleteDirectory(testHomeDir); } private void createTestFolderStructure() throws IOException { @@ -73,21 +87,16 @@ private void createTestFolderStructure() throws IOException { new File(f29.getAbsolutePath() + "/f29_2_virtualbox.box").createNewFile(); } - @AfterClass - public void tearDown() throws IOException { - FileUtils.deleteDirectory(testHomeDir); - } - @DataProvider public Object[][] boxes() { return new Object[][]{ {"f25", Optional.of(new Box("f25", "f25", Arrays.asList( - new BoxVersion("1", Collections.singletonList( + new BoxVersion("1", VERSION_DESCRIPTION, Collections.singletonList( new BoxProvider(composePath("f25", "1", "virtualbox"), "virtualbox", null, null) )), - new BoxVersion("2", Collections.singletonList( + new BoxVersion("2", VERSION_DESCRIPTION, Collections.singletonList( new BoxProvider(composePath("f25", "2", "virtualbox"), "virtualbox", null, null) )) @@ -95,15 +104,15 @@ public Object[][] boxes() { }, {"f26", Optional.of(new Box("f26", "f26", Arrays.asList( - new BoxVersion("1", Collections.singletonList( + new BoxVersion("1", VERSION_DESCRIPTION, Collections.singletonList( new BoxProvider(composePath("f26", "1", "virtualbox"), "virtualbox", null, null) )), - new BoxVersion("2", Collections.singletonList( + new BoxVersion("2", VERSION_DESCRIPTION, Collections.singletonList( new BoxProvider(composePath("f26", "2", "virtualbox"), "virtualbox", null, null) )), - new BoxVersion("3", Collections.singletonList( + new BoxVersion("3", VERSION_DESCRIPTION, Collections.singletonList( new BoxProvider(composePath("f26", "3", "virtualbox"), "virtualbox", null, null) )) @@ -112,14 +121,14 @@ public Object[][] boxes() { {"f27", Optional.empty()}, {"f28", Optional.of(new Box("f28", "f28", Arrays.asList( - new BoxVersion("1", Arrays.asList( + new BoxVersion("1", VERSION_DESCRIPTION, Arrays.asList( new BoxProvider(composePath("f28", "1", "virtualbox"), "virtualbox", null, null), new BoxProvider(composePath("f28", "1", "vmware"), "vmware", null, null) )), - new BoxVersion("2", Collections.singletonList( - new BoxProvider(composePath("f28", "2", "virtualbox"), + new BoxVersion("2", VERSION_DESCRIPTION, Collections.singletonList( + new BoxProvider(composePath("f28", "2", "virtualbox"), "virtualbox", null, null) )) ))) @@ -131,8 +140,8 @@ public Object[][] boxes() { @Test(dataProvider = "boxes") public void givenRepository_whenGetBox_thenGetWhenFound(String boxName, Optional expectedResult) { - BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService()); - + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); Optional providedBox = boxRepository.getBox(boxName); @@ -144,7 +153,8 @@ public void givenRepository_whenGetBox_thenGetWhenFound(String boxName, Optional public void givenSortAscending_whenGetBox_thenVersionsSortedAsc() { testAppProperties.setSort_desc(false); - BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService()); + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); List versions = boxRepository.getBox("f29").get().getVersions(); assertEquals(versions.get(0).getVersion(), "1"); @@ -156,7 +166,8 @@ public void givenSortAscending_whenGetBox_thenVersionsSortedAsc() { public void givenSortDescending_whenGetBox_thenVersionsSortedDesc() { testAppProperties.setSort_desc(true); - BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService()); + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); List versions = boxRepository.getBox("f29").get().getVersions(); assertEquals(versions.get(0).getVersion(), "3"); @@ -165,12 +176,29 @@ public void givenSortDescending_whenGetBox_thenVersionsSortedDesc() { } @Test - public void givenValidRepositoryWithBoxes_whenIndex_thenGetValidBoxes() { - BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService()); + public void givenValidRepositoryWithBoxes_whenGetBoxes_thenGetValidBoxes() { + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); List boxes = boxRepository.getBoxes(); assertTrue(boxes.containsAll(Arrays.asList("f25", "f26", "f28", "f29"))); - assertFalse(boxes.containsAll(Arrays.asList("f27"))); + assertFalse(boxes.containsAll(Collections.singletonList("f27"))); + } + + @Test(expectedExceptions = IllegalStateException.class) + public void givenNonExistingRepositoryDir_whenGetBoxes_thenThrowNotFoundException() { + testAppProperties.setHome("/some/not/existing/dir"); + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); + boxRepository.getBoxes(); + } + + @Test(expectedExceptions = IllegalStateException.class) + public void givenNonExistingRepositoryDir_whenGetBox_thenThrowNotFoundException() { + testAppProperties.setHome("/some/not/existing/dir"); + BoxRepository boxRepository = new FilesystemBoxRepository(testAppProperties, new NoopHashService(), + new NoopDescriptionProvider()); + boxRepository.getBox("invalid_repo_dir"); } private String composePath(String boxName, String version, String provider) { diff --git a/src/test/java/cz/sparko/boxitory/service/FilesystemDescriptionProviderTest.java b/src/test/java/cz/sparko/boxitory/service/FilesystemDescriptionProviderTest.java new file mode 100644 index 0000000..8e53f54 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/service/FilesystemDescriptionProviderTest.java @@ -0,0 +1,166 @@ +package cz.sparko.boxitory.service; + +import cz.sparko.boxitory.conf.AppProperties; +import cz.sparko.boxitory.service.filesystem.FilesystemDescriptionProvider; +import org.apache.commons.io.FileUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static cz.sparko.boxitory.service.filesystem.FilesystemDescriptionProvider.DESCRIPTIONS_FILE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.io.FileUtils.writeStringToFile; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class FilesystemDescriptionProviderTest { + private final String TEST_HOME = "target/test_repository"; + private final String TEST_BOX_PREFIX = "sftp://my_test_server:"; + private File testHomeDir; + + private AppProperties testAppProperties; + + private DescriptionProvider descriptionProvider; + + @BeforeClass + public void setUp() { + testAppProperties = new AppProperties(); + testAppProperties.setHome(TEST_HOME); + testAppProperties.setHost_prefix(TEST_BOX_PREFIX); + testHomeDir = new File(TEST_HOME); + + descriptionProvider = new FilesystemDescriptionProvider(testHomeDir); + } + + @BeforeMethod + public void createTestHomedir() { + testHomeDir.mkdir(); + } + + @AfterMethod + public void cleanTestHomedir() throws IOException { + FileUtils.deleteDirectory(testHomeDir); + } + + @DataProvider + public Object[][] validDescriptions() { + return new Object[][]{ + {"f25", "1", "this is description of version 1"}, + {"f25", "1234", "this is description of version 1234"}, + {"f25", "2", "this is description of version 2"}, + {"f25", "56498981", "this is description of version 56498981"}, + {"f26", "17", "this is desc of v 17"} + }; + } + + @Test(dataProvider = "validDescriptions") + public void givenValidDescriptionFile_whenGetDescription_thenReturnProperDescription(String box, String version, + String description) + throws IOException { + createDirWithValidDescriptions(); + + assertEquals(descriptionProvider.getDescription(box, version).get(), description); + } + + @Test + public void givenNoDescriptionForVersion_whenGetDescription_thenReturnNull() throws IOException { + createDirWithValidDescriptions(); + + assertFalse(descriptionProvider.getDescription("f25", "666").isPresent()); + } + + @Test + public void givenMultipleDescriptionsForVersion_whenGetDescription_thenReturnLatest() throws IOException { + createDirWithValidDescriptions(); + File f25 = new File(testHomeDir.getAbsolutePath() + "/f25"); + File descriptionFile = new File(f25.getAbsolutePath() + "/" + DESCRIPTIONS_FILE); + writeStringToFile(descriptionFile, "1;;;this is second description of version 1\n", UTF_8, true); + writeStringToFile(descriptionFile, "2;;;this is second description of version 2\n", UTF_8, true); + + assertEquals(descriptionProvider.getDescription("f25", "1").get(), "this is second description of version 1"); + assertEquals(descriptionProvider.getDescription("f25", "2").get(), "this is second description of version 2"); + } + + @Test + public void givenInvalidFileButOneLineMatches_whenGet_thenReturnValidDescription() throws IOException { + File versionFile = createDescriptionFileForBox("f27", false); + writeStringToFile(versionFile, "blablabla blebleble fbsajl lsa\n", UTF_8, true); + writeStringToFile(versionFile, "sfqfqs;;;qweeeee\n", UTF_8, true); + writeStringToFile(versionFile, "1;;;this is valid line\n", UTF_8, true); + writeStringToFile(versionFile, "\n", UTF_8, true); + writeStringToFile(versionFile, "1234564789\n", UTF_8, true); + writeStringToFile(versionFile, " \n", UTF_8, true); + + assertEquals(descriptionProvider.getDescription("f27", "1").get(), "this is valid line"); + } + + @Test + public void givenInvalidFile_whenGet_thenReturnNull() throws IOException { + File versionFile = createDescriptionFileForBox("f27", false); + writeStringToFile(versionFile, "blablabla blebleble fbsajl lsa\n", UTF_8, true); + writeStringToFile(versionFile, "sfqfqs;;;qweeeee\n", UTF_8, true); + writeStringToFile(versionFile, "1;;this is valid line\n", UTF_8, true); + writeStringToFile(versionFile, "\n", UTF_8, true); + writeStringToFile(versionFile, "1234564789\n", UTF_8, true); + writeStringToFile(versionFile, " \n", UTF_8, true); + + assertFalse(descriptionProvider.getDescription("f27", "1").isPresent()); + } + + @Test + public void givenNoFileExists_whenGet_thenReturnNull() throws IOException { + createDirWithValidDescriptions(); + assertFalse(descriptionProvider.getDescription("f27", "1").isPresent()); + } + + @Test(expectedExceptions = NullPointerException.class) + public void givenNullBox_whenGet_thenThrowNpe() { + descriptionProvider.getDescription(null, "1"); + } + + @Test(expectedExceptions = NullPointerException.class) + public void givenNullVersion_whenGet_thenThrowNpe() { + descriptionProvider.getDescription("1", null); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void givenEmptyBox_whenGet_thenThrowIae() { + descriptionProvider.getDescription("", "1"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void givenEmptyVersion_whenGet_thenThrowIae() { + descriptionProvider.getDescription("1", ""); + } + + private void createDirWithValidDescriptions() throws IOException { + File descriptionFile = createDescriptionFileForBox("f25", true); + writeStringToFile(descriptionFile, "1;;;this is description of version 1\n", UTF_8, true); + writeStringToFile(descriptionFile, "1234;;;this is description of version 1234\n", UTF_8, true); + writeStringToFile(descriptionFile, "2;;;this is description of version 2\n", UTF_8, true); + writeStringToFile(descriptionFile, "56498981;;;this is description of version 56498981\n", UTF_8, true); + + descriptionFile = createDescriptionFileForBox("f26", true); + writeFileHeader(descriptionFile); + writeStringToFile(descriptionFile, "17;;;this is desc of v 17\n", UTF_8, true); + } + + private File createDescriptionFileForBox(String box, boolean header) throws IOException { + File boxDir = new File(testHomeDir.getAbsolutePath() + "/" + box); + boxDir.mkdir(); + File descriptionsFile = new File(boxDir.getAbsolutePath() + "/" + DESCRIPTIONS_FILE); + if (header) { + writeFileHeader(descriptionsFile); + } + return descriptionsFile; + } + + private void writeFileHeader(File descriptionFile) throws IOException { + writeStringToFile(descriptionFile, "version;;;description\n", UTF_8); + } +} \ No newline at end of file diff --git a/src/test/java/cz/sparko/boxitory/service/FilesystemDigestHashServiceTest.java b/src/test/java/cz/sparko/boxitory/service/FilesystemDigestHashServiceTest.java index c4c274d..205618b 100644 --- a/src/test/java/cz/sparko/boxitory/service/FilesystemDigestHashServiceTest.java +++ b/src/test/java/cz/sparko/boxitory/service/FilesystemDigestHashServiceTest.java @@ -1,7 +1,10 @@ package cz.sparko.boxitory.service; import cz.sparko.boxitory.conf.AppProperties; +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.filesystem.FilesystemDigestHashService; import org.apache.commons.io.FileUtils; +import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -13,7 +16,9 @@ import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.MD5; import static org.testng.Assert.assertEquals; @SpringBootTest @@ -55,17 +60,17 @@ public void tearDown() throws IOException { public Object[][] filesAndHashes() { return new Object[][]{ { - "MD5", + HashAlgorithm.MD5, new File(testHomeDir.getAbsolutePath() + "/f25/f25_1_virtualbox.box"), "86462c346f1358ddbf4f137fb5da43cf" }, { - "SHA-1", + HashAlgorithm.SHA1, new File(testHomeDir.getAbsolutePath() + "/f25/f25_1_virtualbox.box"), "6efeafd3d3304cf5d7fd37db2a7ddbaac09f425d" }, { - "SHA-256", + HashAlgorithm.SHA256, new File(testHomeDir.getAbsolutePath() + "/f25/f25_1_virtualbox.box"), "ae4fe7f29f683d3901d4c620ef2e3c7ed17ebb6813158efd6a16f81b71a0aa43" } @@ -73,13 +78,34 @@ public Object[][] filesAndHashes() { } @Test(dataProvider = "filesAndHashes") - public void givenHashService_whenGetChecksum_thenChecksumsAreEquals(String algorithm, File file, String - expectedChecksum) throws NoSuchAlgorithmException { - HashService hashService = new FilesystemDigestHashService(MessageDigest.getInstance(algorithm), new AppProperties()); + public void givenHashService_whenGetChecksum_thenChecksumsAreEquals( + HashAlgorithm algorithm, File file, String expectedChecksum) throws NoSuchAlgorithmException { + HashService hashService = new FilesystemDigestHashService( + MessageDigest.getInstance(algorithm.getMessageDigestName()), algorithm, 1024); String checksum = hashService.getChecksum(file.getAbsolutePath()); assertEquals(checksum, expectedChecksum); } + @Test + public void givenHashService_whenLoadHashReturnsHash_thenHashStoreLoadAndPersistAreCalled() + throws NoSuchAlgorithmException { + final String box = "box"; + final String hash = "hash"; + final HashAlgorithm algorithm = MD5; + + AppProperties properties = new AppProperties(); + properties.setChecksum(algorithm); + + HashStore hashStore = Mockito.mock(HashStore.class); + Mockito.when(hashStore.loadHash(box, algorithm)).thenReturn(Optional.of(hash)); + + HashService hashService = new FilesystemDigestHashService( + MessageDigest.getInstance(algorithm.getMessageDigestName()), algorithm, hashStore); + hashService.getChecksum(box); + + Mockito.verify(hashStore, Mockito.times(1)).loadHash(box, algorithm); + Mockito.verify(hashStore, Mockito.times(1)).persist(box, hash, algorithm); + } } diff --git a/src/test/java/cz/sparko/boxitory/service/FilesystemHashStoreTest.java b/src/test/java/cz/sparko/boxitory/service/FilesystemHashStoreTest.java new file mode 100644 index 0000000..66bea92 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/service/FilesystemHashStoreTest.java @@ -0,0 +1,177 @@ +package cz.sparko.boxitory.service; + +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.filesystem.FilesystemHashStore; +import org.apache.commons.io.FileUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.List; +import java.util.Optional; + +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.DISABLED; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.MD5; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.SHA1; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.SHA256; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class FilesystemHashStoreTest { + private final String TEST_HOME = "test_repository"; + private File testHomeDir; + private String validBoxAbsPath; + private String validBoxFilename; + + + private HashStore hashStore; + + @BeforeMethod + public void setUpTest() throws IOException { + this.hashStore = new FilesystemHashStore(); + + testHomeDir = new File(TEST_HOME); + + createTestFolderStructure(); + } + + @AfterMethod + public void tearDownTest() throws IOException { + FileUtils.deleteDirectory(testHomeDir); + } + + private void createTestFolderStructure() throws IOException { + testHomeDir.mkdir(); + File f25 = new File(testHomeDir.getAbsolutePath() + "/f25"); + + f25.mkdir(); + + validBoxFilename = "f25_1_virtualbox.box"; + + File f25box = new File(f25.getAbsolutePath() + "/" + validBoxFilename); + f25box.createNewFile(); + + validBoxAbsPath = f25box.getAbsolutePath(); + } + + @DataProvider + public Object[][] validData() { + return new Object[][]{{MD5}, {SHA1}, {SHA256}}; + } + + @Test(dataProvider = "validData") + public void givenExistingBoxAndEnabledAlg_whenPersist_thenHashIsProperlyStored + (HashAlgorithm algorithm) throws IOException { + final String testHashValue = "blabol"; + + this.hashStore.persist(validBoxAbsPath, testHashValue, algorithm); + + File expectedFile = new File(validBoxAbsPath + algorithm.getFileExtension()); + + assertTrue(expectedFile.exists()); + assertTrue(expectedFile.isFile()); + + List expectedFileLines = Files.readAllLines(expectedFile.toPath(), Charset.defaultCharset()); + + assertEquals(expectedFileLines.size(), 1); + assertTrue(expectedFileLines.get(0).split(" ")[0].equals(testHashValue)); + } + + @Test(dataProvider = "validData") + public void givenExistingBoxEnabledAlgAndFileAlreadyExist_whenPersist_thenOriginalFileIsNotReplaced + (HashAlgorithm algorithm) throws IOException { + final String testHashValue = "blabol"; + + File existingFile = new File(validBoxAbsPath + algorithm.getFileExtension()); + existingFile.createNewFile(); + try (FileWriter writer = new FileWriter(existingFile)) { + writer.write(testHashValue); + } + + + this.hashStore.persist(validBoxAbsPath, "some_new_hash", algorithm); + + assertTrue(existingFile.exists()); + assertTrue(existingFile.isFile()); + + List expectedFileLines = Files.readAllLines(existingFile.toPath(), Charset.defaultCharset()); + + assertEquals(expectedFileLines.size(), 1); + assertTrue(expectedFileLines.get(0).equals(testHashValue)); + } + + @Test(expectedExceptions = IllegalStateException.class) + public void givenExistingBoxAndDisabledAlg_whenPersist_thenThrowISE() { + final String testHashValue = "blabol"; + + this.hashStore.persist(validBoxAbsPath, testHashValue, DISABLED); + } + + @Test(expectedExceptions = IllegalStateException.class) + public void givenNonExistingBoxAndEnabledAlg_whenPersist_thenIllegalStateExceptionThrown() { + this.hashStore.persist(validBoxAbsPath + "noise", "blabol", MD5); + } + + @DataProvider + public Object[][] wrongHashFileContentFormats() { + return new Object[][] { + {"this_is_hash this_file_does_not_exists"}, + {"this_is_just_hash_without_filename"} + }; + } + + @Test(dataProvider = "wrongHashFileContentFormats") + public void givenWrongFileFormat_whenLoad_thenReturnEmpty(String content) throws IOException { + HashAlgorithm algorithm = MD5; + createTestHashFile(content, validBoxAbsPath + algorithm.getFileExtension()); + + Optional loadedHash = hashStore.loadHash(validBoxAbsPath, algorithm); + assertFalse(loadedHash.isPresent(), "Should be empty, but is[" + loadedHash.orElse("") + "]"); + } + + @Test + public void givenValidHashFile_whenLoad_thenProperlyLoaded() throws IOException { + HashAlgorithm algorithm = MD5; + final String hash = "this_is_fake_hash"; + createTestHashFile(hash + " " + validBoxFilename, validBoxAbsPath + algorithm.getFileExtension()); + + Optional loadedHash = hashStore.loadHash(validBoxAbsPath, algorithm); + assertTrue(loadedHash.isPresent()); + assertEquals(loadedHash.get(), hash, "hash is properly loaded " + loadedHash.get()); + } + + @Test + public void givenHashFileWithWrongName_whenLoad_thenNothingLoaded() throws IOException { + HashAlgorithm algorithm = MD5; + final String hash = "this_is_fake_hash"; + createTestHashFile(hash + " " + validBoxFilename, validBoxAbsPath + algorithm.getFileExtension() + "_noise"); + + Optional loadedHash = hashStore.loadHash(validBoxAbsPath, algorithm); + assertFalse(loadedHash.isPresent()); + } + + @Test + public void givenHashFileWithDifferentExtension_whenLoad_thenNothingLoaded() throws IOException { + HashAlgorithm algorithm = MD5; + final String hash = "this_is_fake_hash"; + createTestHashFile(hash + " " + validBoxFilename, validBoxAbsPath + SHA1.getFileExtension()); + + Optional loadedHash = hashStore.loadHash(validBoxAbsPath, algorithm); + assertFalse(loadedHash.isPresent()); + } + + private File createTestHashFile(String content, String fileFullPath) throws IOException { + File hashFile = new File(fileFullPath); + try (FileWriter fileWriter = new FileWriter(hashFile)) { + fileWriter.write(content); + } + return hashFile; + } +} \ No newline at end of file diff --git a/src/test/java/cz/sparko/boxitory/service/HashServiceFactoryTest.java b/src/test/java/cz/sparko/boxitory/service/HashServiceFactoryTest.java index aa0bd20..bfc9aba 100644 --- a/src/test/java/cz/sparko/boxitory/service/HashServiceFactoryTest.java +++ b/src/test/java/cz/sparko/boxitory/service/HashServiceFactoryTest.java @@ -2,6 +2,10 @@ import cz.sparko.boxitory.conf.AppProperties; import cz.sparko.boxitory.factory.HashServiceFactory; +import cz.sparko.boxitory.service.HashService.HashAlgorithm; +import cz.sparko.boxitory.service.filesystem.FilesystemDigestHashService; +import cz.sparko.boxitory.service.noop.NoopHashService; +import cz.sparko.boxitory.service.noop.NoopHashStore; import org.springframework.boot.test.context.SpringBootTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -10,6 +14,10 @@ import java.security.NoSuchAlgorithmException; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.DISABLED; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.MD5; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.SHA1; +import static cz.sparko.boxitory.service.HashService.HashAlgorithm.SHA256; import static org.testng.Assert.assertEquals; @SpringBootTest @@ -18,26 +26,21 @@ public class HashServiceFactoryTest { @DataProvider public Object[][] hashServiceTypes() throws NoSuchAlgorithmException { return new Object[][]{ - {"md5", new FilesystemDigestHashService(MessageDigest.getInstance("MD5"), new AppProperties())}, - {"sha1", new FilesystemDigestHashService(MessageDigest.getInstance("SHA-1"), new AppProperties())}, - {"sha256", new FilesystemDigestHashService(MessageDigest.getInstance("SHA-256"), new AppProperties())}, - {"disabled", new NoopHashService()} + {MD5, new FilesystemDigestHashService(MessageDigest.getInstance("MD5"), MD5)}, + {SHA1, new FilesystemDigestHashService(MessageDigest.getInstance("SHA-1"), SHA1)}, + {SHA256, new FilesystemDigestHashService(MessageDigest.getInstance("SHA-256"), SHA256)}, + {DISABLED, new NoopHashService()} }; } @Test(dataProvider = "hashServiceTypes") - public void givenFactory_whenCreateHashService_thenGetExpectedInstance(String type, HashService expectedService) throws NoSuchAlgorithmException { + public void givenFactory_whenCreateHashService_thenGetExpectedInstance(HashAlgorithm type, + HashService expectedService) + throws NoSuchAlgorithmException { AppProperties appProperties = new AppProperties(); appProperties.setChecksum(type); - HashService hashService = HashServiceFactory.createHashService(appProperties); + HashService hashService = HashServiceFactory.createHashService(appProperties, new NoopHashStore()); assertEquals(hashService, expectedService); } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void givenFactory_whenCreateUnsupportedHashService_thenExceptionIsThrown() throws NoSuchAlgorithmException { - AppProperties appProperties = new AppProperties(); - appProperties.setChecksum("foo"); - HashServiceFactory.createHashService(appProperties); - } } diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/AbstractIntegrationTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/AbstractIntegrationTest.java new file mode 100644 index 0000000..4292e51 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/AbstractIntegrationTest.java @@ -0,0 +1,78 @@ +package cz.sparko.boxitory.test.e2e; + +import cz.sparko.boxitory.App; +import cz.sparko.boxitory.conf.AppProperties; +import cz.sparko.boxitory.controller.BoxController; +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.web.servlet.MockMvc; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +@ContextConfiguration(classes = App.class) +@WebMvcTest(controllers = BoxController.class) +@AutoConfigureMockMvc(secure = false) +@TestPropertySource(locations = "classpath:test.properties") +@Test(groups = "e2e") +public abstract class AbstractIntegrationTest extends AbstractTestNGSpringContextTests { + public static final String UTF8_CHARSET = ";charset=UTF-8"; + + @Autowired + public AppProperties appProperties; + + @Autowired + public MockMvc mockMvc; + + @BeforeMethod + public void setUp() throws IOException { + createFolderStructure(); + } + + @AfterMethod + public void tearDown() throws IOException { + destroyFolderStructure(); + } + + public void createRepositoryDir() { + new File(appProperties.getHome()).mkdir(); + } + + public void createFolderStructure() throws IOException { + createRepositoryDir(); + } + + public void destroyFolderStructure() throws IOException { + FileUtils.deleteDirectory(new File(appProperties.getHome())); + } + + @Configuration + static class TestConfig { + @Bean + public AppProperties appProperties() { + return new AppProperties(); + } + } + + public File createDirInRepository(String vmName) { + File vmDir = new File(appProperties.getHome() + File.separator + vmName); + vmDir.mkdir(); + return vmDir; + } + + public File createFile(String filePath) throws IOException { + File testFile = new File(filePath); + testFile.createNewFile(); + return testFile; + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/DontPersistChecksumTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/DontPersistChecksumTest.java new file mode 100644 index 0000000..d049c1e --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/DontPersistChecksumTest.java @@ -0,0 +1,48 @@ +package cz.sparko.boxitory.test.e2e; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.testng.Assert.assertFalse; + +@TestPropertySource(properties = { + "box.checksum=md5", + "box.checksum_persist=false" +}) +public class DontPersistChecksumTest extends AbstractIntegrationTest { + private static final String VM = "vm"; + private static final String BOX = "vm_1_vb.box"; + private static final String CHECKSUM_FILE = "vm_1_vb.box.md5"; + + private File vmDir; + + @Override + public void createFolderStructure() throws IOException { + super.createFolderStructure(); + vmDir = createDirInRepository(VM); + File box = createFile(vmDir.getPath() + File.separator + BOX); + try (FileWriter fw = new FileWriter(box)) { + fw.write("blabol"); + } + } + + @Test + public void givenMd5WithPersist_whenGetBox_thenFileWithChecksumIsStored() throws Exception { + File checksumFile = new File(vmDir + File.separator + CHECKSUM_FILE); + assertFalse(checksumFile.exists()); + + mockMvc.perform(get("/" + VM)) + .andDo(print()) + .andExpect(jsonPath("$.versions[0].providers[0].checksum", is("f34835fa588de624b2782bd5307c344c"))); + + assertFalse(checksumFile.exists()); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/MultiProviderTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/MultiProviderTest.java new file mode 100644 index 0000000..b453343 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/MultiProviderTest.java @@ -0,0 +1,81 @@ +package cz.sparko.boxitory.test.e2e; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.TEXT_HTML; +import static org.springframework.http.MediaType.TEXT_PLAIN; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@TestPropertySource(properties = {"box.sort_desc=true"}) +public class MultiProviderTest extends AbstractIntegrationTest { + + private final String VM = "vm"; + private final String VM_1_VBOX = VM + "_1_virtualbox.box"; + private final String VM_2_VBOX = VM + "_2_virtualbox.box"; + private final String VM_2_LVIRT = VM + "_2_libvirt.box"; + + @Override + public void createFolderStructure() throws IOException { + createRepositoryDir(); + File vmDir = createDirInRepository(VM); + createFile(vmDir.getPath() + File.separator + VM_1_VBOX); + createFile(vmDir.getPath() + File.separator + VM_2_VBOX); + createFile(vmDir.getPath() + File.separator + VM_2_LVIRT); + } + + @Test + public void givenMultiProviders_whenIndex_thenReturnListWithVm() throws Exception { + mockMvc.perform(get("/")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_HTML + UTF8_CHARSET)) + .andExpect(view().name("index")) + .andExpect(content().string(containsString(VM))); + } + + @Test + public void givenMultiProviders_whenBox_thenReturnListOfVmVersions() throws Exception { + mockMvc.perform(get("/vm")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.name", is(VM))) + .andExpect(jsonPath("$.description", is(VM))) + .andExpect(jsonPath("$.versions", hasSize(2))) + .andExpect(jsonPath("$.versions[0].version", is("2"))) + .andExpect(jsonPath("$.versions[0].providers", hasSize(2))) + .andExpect(jsonPath("$.versions[0].providers[?(@.name == \'libvirt\' && " + + "@.url =~ /.*" + appProperties.getHost_prefix() + ".*/i && " + + "@.url =~ /.*" + VM_2_LVIRT + ".*/i)]").exists()) + .andExpect(jsonPath("$.versions[0].providers[?(@.name == \'virtualbox\' && " + + "@.url =~ /.*" + appProperties.getHost_prefix() + ".*/i && " + + "@.url =~ /.*" + VM_2_VBOX + ".*/i)]").exists()) + .andExpect(jsonPath("$.versions[1].version", is("1"))) + .andExpect(jsonPath("$.versions[1].providers", hasSize(1))) + .andExpect(jsonPath("$.versions[1].providers[0].name", is("virtualbox"))) + .andExpect(jsonPath("$.versions[1].providers[0].url", containsString(appProperties.getHost_prefix()))) + .andExpect(jsonPath("$.versions[1].providers[0].url", containsString(VM_1_VBOX))); + } + + @Test + public void givenMultiProviders_whenLatestVersion_thenReturnLatestVersionNumber() throws Exception { + mockMvc.perform(get("/vm/latestVersion")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_PLAIN + UTF8_CHARSET)) + .andExpect(content().string("2")); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/MultiVmTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/MultiVmTest.java new file mode 100644 index 0000000..e5299fa --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/MultiVmTest.java @@ -0,0 +1,110 @@ +package cz.sparko.boxitory.test.e2e; + +import cz.sparko.boxitory.conf.AppProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.TEXT_HTML; +import static org.springframework.http.MediaType.TEXT_PLAIN; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@TestPropertySource(properties = {"box.sort_desc=true"}) +public class MultiVmTest extends AbstractIntegrationTest { + + private final String VM1 = "vm1"; + private final String VM2 = "vm2"; + private final String VM1_1_VBOX = VM1 + "_1_virtualbox.box"; + private final String VM1_2_VBOX = VM1 + "_2_virtualbox.box"; + private final String VM2_17_VBOX = VM2 + "_17_virtualbox.box"; + + @Override + public void createFolderStructure() throws IOException { + createRepositoryDir(); + File vm1Dir = createDirInRepository(VM1); + File vm2Dir = createDirInRepository(VM2); + createFile(vm1Dir.getPath() + File.separator + VM1_1_VBOX); + createFile(vm1Dir.getPath() + File.separator + VM1_2_VBOX); + createFile(vm2Dir.getPath() + File.separator + VM2_17_VBOX); + } + + @Autowired + AppProperties appProperties; + + @Test + public void givenMultiProviders_whenIndex_thenReturnListWithVm() throws Exception { + mockMvc.perform(get("/")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_HTML + UTF8_CHARSET)) + .andExpect(view().name("index")) + .andExpect(content().string(containsString(VM1))) + .andExpect(content().string(containsString(VM2))); + } + + @Test + public void givenMultiProviders_whenVm1Box_thenReturnListOfVmVersions() throws Exception { + mockMvc.perform(get("/" + VM1)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.name", is(VM1))) + .andExpect(jsonPath("$.description", is(VM1))) + .andExpect(jsonPath("$.versions", hasSize(2))) + .andExpect(jsonPath("$.versions[0].version", is("2"))) + .andExpect(jsonPath("$.versions[0].providers", hasSize(1))) + .andExpect(jsonPath("$.versions[0].providers[0].name", is("virtualbox"))) + .andExpect(jsonPath("$.versions[0].providers[0].url", containsString(appProperties.getHost_prefix()))) + .andExpect(jsonPath("$.versions[0].providers[0].url", containsString(VM1_2_VBOX))) + .andExpect(jsonPath("$.versions[1].version", is("1"))) + .andExpect(jsonPath("$.versions[1].providers[0].name", is("virtualbox"))) + .andExpect(jsonPath("$.versions[1].providers[0].url", containsString(appProperties.getHost_prefix()))) + .andExpect(jsonPath("$.versions[1].providers[0].url", containsString(VM1_1_VBOX))); + } + + @Test + public void givenMultiProviders_whenVm2Box_thenReturnListOfVmVersions() throws Exception { + mockMvc.perform(get("/" + VM2)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.name", is(VM2))) + .andExpect(jsonPath("$.description", is(VM2))) + .andExpect(jsonPath("$.versions", hasSize(1))) + .andExpect(jsonPath("$.versions[0].version", is("17"))) + .andExpect(jsonPath("$.versions[0].providers", hasSize(1))) + .andExpect(jsonPath("$.versions[0].providers[0].name", is("virtualbox"))) + .andExpect(jsonPath("$.versions[0].providers[0].url", containsString(appProperties.getHost_prefix()))) + .andExpect(jsonPath("$.versions[0].providers[0].url", containsString(VM2_17_VBOX))); + } + + @Test + public void givenMultiProviders_whenLatestVersionVm1_thenReturnLatestVersionNumber() throws Exception { + mockMvc.perform(get("/" + VM1 + "/latestVersion")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_PLAIN + UTF8_CHARSET)) + .andExpect(content().string("2")); + } + + @Test + public void givenMultiProviders_whenLatestVersionVm2_thenReturnLatestVersionNumber() throws Exception { + mockMvc.perform(get("/" + VM2 + "/latestVersion")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_PLAIN + UTF8_CHARSET)) + .andExpect(content().string("17")); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/PersistChecksumTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/PersistChecksumTest.java new file mode 100644 index 0000000..ecb96cc --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/PersistChecksumTest.java @@ -0,0 +1,71 @@ +package cz.sparko.boxitory.test.e2e; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +@TestPropertySource(properties = { + "box.checksum=md5", + "box.checksum_persist=true" +}) +public class PersistChecksumTest extends AbstractIntegrationTest { + private static final String VM = "vm"; + private static final String BOX = "vm_1_vb.box"; + private static final String CHECKSUM_FILE = "vm_1_vb.box.md5"; + + private File vmDir; + + @Override + public void createFolderStructure() throws IOException { + super.createFolderStructure(); + vmDir = createDirInRepository(VM); + File box = createFile(vmDir.getPath() + File.separator + BOX); + try (FileWriter fw = new FileWriter(box)) { + fw.write("blabol"); + } + } + + @Test + public void givenMd5WithPersist_whenGetBox_thenFileWithChecksumIsStored() throws Exception { + File checksumFile = new File(vmDir + File.separator + CHECKSUM_FILE); + assertFalse(checksumFile.exists()); + + mockMvc.perform(get("/" + VM)); + + assertTrue(checksumFile.exists()); + + List checksumFileContent = Files.readAllLines(checksumFile.toPath()); + System.out.println(checksumFileContent); + assertEquals(checksumFileContent.size(), 1); + assertTrue(checksumFileContent.get(0).endsWith(BOX)); + assertTrue(checksumFileContent.get(0).startsWith("f34835fa588de624b2782bd5307c344c")); + } + + @Test + public void givenMd5WithPersist_whenChecksumFileStored_thenChecksumReadFromFile() throws Exception { + final String FAKE_CHECKSUM = "fake_checksum"; + File checksumFile = new File(vmDir + File.separator + CHECKSUM_FILE); + assertFalse(checksumFile.exists()); + + try (FileWriter fw = new FileWriter(checksumFile)) { + fw.write(FAKE_CHECKSUM + " " + BOX); + } + + mockMvc.perform(get("/" + VM)) + .andDo(print()) + .andExpect(jsonPath("$.versions[0].providers[0].checksum", is(FAKE_CHECKSUM))); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/WrongBoxTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/WrongBoxTest.java new file mode 100644 index 0000000..67f4e07 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/WrongBoxTest.java @@ -0,0 +1,16 @@ +package cz.sparko.boxitory.test.e2e; + +import org.testng.annotations.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class WrongBoxTest extends AbstractIntegrationTest{ + @Test + public void givenValidRepository_whenRequestWrongBox_then404() throws Exception { + mockMvc.perform(get("/box")) + .andDo(print()) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/WrongRepositoryDirTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/WrongRepositoryDirTest.java new file mode 100644 index 0000000..4b35e94 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/WrongRepositoryDirTest.java @@ -0,0 +1,36 @@ +package cz.sparko.boxitory.test.e2e; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = {"box.home=this/is/not/valid/repository/dir"}) +public class WrongRepositoryDirTest extends AbstractIntegrationTest { + + @Override + public void createFolderStructure() { } + + @Test + public void givenWrongRepoDir_whenRequestRoot_then500() throws Exception { + mockMvc.perform(get("/")) + .andDo(print()) + .andExpect(status().is5xxServerError()); + } + + @Test + public void givenWrongRepoDir_whenRequestBox_then500() throws Exception { + mockMvc.perform(get("/box")) + .andDo(print()) + .andExpect(status().is5xxServerError()); + } + + @Test + public void givenWrongRepoDir_whenRequestBoxLatestVersion_then500() throws Exception { + mockMvc.perform(get("/box")) + .andDo(print()) + .andExpect(status().is5xxServerError()); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeMd5Test.java b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeMd5Test.java new file mode 100644 index 0000000..9972f27 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeMd5Test.java @@ -0,0 +1,11 @@ +package cz.sparko.boxitory.test.e2e.hashtype; + +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = {"box.checksum=md5"}) +public class HashTypeMd5Test extends HashTypeTest { + @Override + String expectedAlg() { + return "md5"; + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha1Test.java b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha1Test.java new file mode 100644 index 0000000..cb9ecc0 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha1Test.java @@ -0,0 +1,11 @@ +package cz.sparko.boxitory.test.e2e.hashtype; + +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = {"box.checksum=sha1"}) +public class HashTypeSha1Test extends HashTypeTest { + @Override + String expectedAlg() { + return "sha1"; + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha256Test.java b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha256Test.java new file mode 100644 index 0000000..7c2973d --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeSha256Test.java @@ -0,0 +1,11 @@ +package cz.sparko.boxitory.test.e2e.hashtype; + +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = {"box.checksum=sha256"}) +public class HashTypeSha256Test extends HashTypeTest { + @Override + String expectedAlg() { + return "sha256"; + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeTest.java new file mode 100644 index 0000000..0589af9 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/hashtype/HashTypeTest.java @@ -0,0 +1,38 @@ +package cz.sparko.boxitory.test.e2e.hashtype; + +import cz.sparko.boxitory.test.e2e.AbstractIntegrationTest; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +abstract public class HashTypeTest extends AbstractIntegrationTest { + private final String VM = "vm"; + private final String VM_1_VBOX = VM + "_1_virtualbox.box"; + + @Override + public void createFolderStructure() throws IOException { + createRepositoryDir(); + File vmDir = createDirInRepository(VM); + createFile(vmDir.getPath() + File.separator + VM_1_VBOX); + } + + abstract String expectedAlg(); + + @Test + public void givenHashAlg_whenGetBox_thenHashMethodIsCorrect() throws Exception { + mockMvc.perform(get("/" + VM)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.versions[0].providers[0].checksum_type", is(expectedAlg()))); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortAscTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortAscTest.java new file mode 100644 index 0000000..54532e9 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortAscTest.java @@ -0,0 +1,7 @@ +package cz.sparko.boxitory.test.e2e.latestversion; + +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = {"box.sort_desc=false"}) +public class LatestVersionSortAscTest extends LatestVersionSortDescTest { +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortDescTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortDescTest.java new file mode 100644 index 0000000..66394a3 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/latestversion/LatestVersionSortDescTest.java @@ -0,0 +1,53 @@ +package cz.sparko.boxitory.test.e2e.latestversion; + + +import cz.sparko.boxitory.test.e2e.AbstractIntegrationTest; +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static org.springframework.http.MediaType.TEXT_PLAIN; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = {"box.sort_desc=true"}) +public class LatestVersionSortDescTest extends AbstractIntegrationTest { + + private final String VM = "vm"; + private final String VM_1_VBOX = VM + "_1_virtualbox.box"; + private final String VM_2_VBOX = VM + "_2_virtualbox.box"; + private final String VM_3_VBOX = VM + "_3_virtualbox.box"; + private final String VM_5_VBOX = VM + "_5_virtualbox.box"; + private final String VM_12_VBOX = VM + "_12_virtualbox.box"; + + @Override + public void createFolderStructure() throws IOException { + createRepositoryDir(); + File vmDir = createDirInRepository(VM); + createFile(vmDir.getPath() + File.separator + VM_5_VBOX); + createFile(vmDir.getPath() + File.separator + VM_3_VBOX); + createFile(vmDir.getPath() + File.separator + VM_1_VBOX); + createFile(vmDir.getPath() + File.separator + VM_12_VBOX); + createFile(vmDir.getPath() + File.separator + VM_2_VBOX); + } + + @Test + public void givenValidRepo_whenLatestVersion_thenGetsLatestVersion() throws Exception { + mockMvc.perform(get("/" + VM + "/latestVersion")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(TEXT_PLAIN + UTF8_CHARSET)) + .andExpect(content().string("12")); + } + + @Test + public void givenValidRepo_whenLatestVersionOnNonExistingBox_then404() throws Exception { + mockMvc.perform(get("/this_box_dont_exist/latestVersion")) + .andDo(print()) + .andExpect(status().is4xxClientError()); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortAscTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortAscTest.java new file mode 100644 index 0000000..4ccd697 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortAscTest.java @@ -0,0 +1,29 @@ +package cz.sparko.boxitory.test.e2e.versionsort; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = {"box.sort_desc=false"}) +public class VersionSortAscTest extends VersionSortTest { + + @Test + public void givenSortAsc_whenGetBox_thenVersionsSortedAsc() throws Exception { + mockMvc.perform(get("/" + VM)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.versions[0].version", is("1"))) + .andExpect(jsonPath("$.versions[1].version", is("2"))) + .andExpect(jsonPath("$.versions[2].version", is("3"))) + .andExpect(jsonPath("$.versions[3].version", is("5"))) + .andExpect(jsonPath("$.versions[4].version", is("12"))); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortDescTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortDescTest.java new file mode 100644 index 0000000..83348e9 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortDescTest.java @@ -0,0 +1,29 @@ +package cz.sparko.boxitory.test.e2e.versionsort; + +import org.springframework.test.context.TestPropertySource; +import org.testng.annotations.Test; + +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = {"box.sort_desc=true"}) +public class VersionSortDescTest extends VersionSortTest { + + @Test + public void givenSortAsc_whenGetBox_thenVersionsSortedAsc() throws Exception { + mockMvc.perform(get("/" + VM)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.versions[0].version", is("12"))) + .andExpect(jsonPath("$.versions[1].version", is("5"))) + .andExpect(jsonPath("$.versions[2].version", is("3"))) + .andExpect(jsonPath("$.versions[3].version", is("2"))) + .andExpect(jsonPath("$.versions[4].version", is("1"))); + } +} diff --git a/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortTest.java b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortTest.java new file mode 100644 index 0000000..2e13580 --- /dev/null +++ b/src/test/java/cz/sparko/boxitory/test/e2e/versionsort/VersionSortTest.java @@ -0,0 +1,26 @@ +package cz.sparko.boxitory.test.e2e.versionsort; + +import cz.sparko.boxitory.test.e2e.AbstractIntegrationTest; + +import java.io.File; +import java.io.IOException; + +abstract public class VersionSortTest extends AbstractIntegrationTest { + final String VM = "vm"; + final String VM_1_VBOX = VM + "_1_virtualbox.box"; + final String VM_2_VBOX = VM + "_2_virtualbox.box"; + final String VM_3_VBOX = VM + "_3_virtualbox.box"; + final String VM_5_VBOX = VM + "_5_virtualbox.box"; + final String VM_12_VBOX = VM + "_12_virtualbox.box"; + + @Override + public void createFolderStructure() throws IOException { + createRepositoryDir(); + File vmDir = createDirInRepository(VM); + createFile(vmDir.getPath() + File.separator + VM_5_VBOX); + createFile(vmDir.getPath() + File.separator + VM_12_VBOX); + createFile(vmDir.getPath() + File.separator + VM_3_VBOX); + createFile(vmDir.getPath() + File.separator + VM_1_VBOX); + createFile(vmDir.getPath() + File.separator + VM_2_VBOX); + } +} diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..dfb7572 --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,8 @@ +box.home=test_repository +box.host_prefix=test_prefix + +box.sort_desc=true + +box.checksum=DISABLED +box.checksum_buffer_size=1024 +box.checksum_persist=true