diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 9dda3b659b..8b4bf8dee1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,2 @@ +# Keep this version number in sync with Jenkinsfile distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index cd02d15487..50a2374544 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,16 +11,14 @@ node() { // No specific label properties([ // Keep only the last 10 build to preserve space - buildDiscarder(logRotator(numToKeepStr: '10')), + buildDiscarder(logRotator(numToKeepStr: '10')) ]) - catchError { + timeout(activity: true, time: 20, unit: 'MINUTES') { - Maven mvn = setupMavenBuild() - // Maven build specified it must be 1.8.0-101 or newer - def javaHome = tool 'JDK-1.8.0-101+' + catchError { - withEnv(["JAVA_HOME=${javaHome}", "PATH=${env.JAVA_HOME}/bin:${env.PATH}"]) { + Maven mvn = setupMavenBuild() stage('Checkout') { checkout scm @@ -47,21 +45,22 @@ node() { // No specific label } } } + + // Archive Unit and integration test results, if any + junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' + + // Find maven warnings and visualize in job + warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true + + mailIfStatusChanged(commitAuthorEmail) } - - // Archive Unit and integration test results, if any - junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' - - // Find maven warnings and visualize in job - warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true - - mailIfStatusChanged(commitAuthorEmail) } String mainBranch Maven setupMavenBuild() { - Maven mvn = new MavenWrapper(this) + // Keep this version number in sync with .mvn/maven-wrapper.properties + Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") if (mainBranch.equals(env.BRANCH_NAME)) { // Release starts javadoc, which takes very long, so do only for certain branches diff --git a/pom.xml b/pom.xml index 8ca4318230..944ebb6eb6 100644 --- a/pom.xml +++ b/pom.xml @@ -430,7 +430,9 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.17 + + 1.16 org.codehaus.mojo.signature diff --git a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java index 94b31ac844..454003c922 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java +++ b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java @@ -33,9 +33,8 @@ package sonia.scm.plugin; //~--- JDK imports ------------------------------------------------------------ -import java.net.URL; - import javax.servlet.ServletContext; +import java.net.URL; /** * The WebResourceLoader is able to load web resources. The resources are loaded @@ -53,9 +52,11 @@ public interface WebResourceLoader * Returns a {@link URL} for the given path. The method will return null if no * resources could be found for the given path. * + * Note: The path is a web path and uses "/" as path separator + * * @param path resource path * * @return url object for the given path or null */ - public URL getResource(String path); + URL getResource(String path); } diff --git a/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java b/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java new file mode 100644 index 0000000000..16dca8e910 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/CRLFInjectionException.java @@ -0,0 +1,8 @@ +package sonia.scm.util; + +public class CRLFInjectionException extends IllegalArgumentException{ + + public CRLFInjectionException(String message) { + super(message); + } +} diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index bc3f4e74cd..2744addc62 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -344,8 +344,7 @@ public final class HttpUtil "parameter \"{}\" contains a character which could be an indicator for a crlf injection", parameter); - throw new IllegalArgumentException( - "parameter contains an illegal character"); + throw new CRLFInjectionException("parameter contains an illegal character"); } } diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index a5d9575efa..d9a6846795 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -12,6 +12,8 @@ public class VndMediaType { private static final String SUBTYPE_PREFIX = "vnd.scmm-"; public static final String PREFIX = TYPE + "/" + SUBTYPE_PREFIX; public static final String SUFFIX = "+json;v=" + VERSION; + public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; + public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; @@ -23,6 +25,7 @@ public class VndMediaType { public static final String TAG = PREFIX + "tag" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; + public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 85764291a7..fc095828b3 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -37,6 +37,7 @@ public class RepositoryAccessITCase { private final String repositoryType; private File folder; + private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -48,9 +49,15 @@ public class RepositoryAccessITCase { } @Before - public void initClient() { + public void init() { TestData.createDefault(); folder = tempFolder.getRoot(); + repositoryGetRequest = RepositoryRequests.start() + .given() + .url(TestData.getDefaultRepositoryUrl(repositoryType)) + .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) + .get() + .assertStatusCode(HttpStatus.SC_OK); } @Test @@ -246,5 +253,64 @@ public class RepositoryAccessITCase { assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits } + + @Test + public void shouldFindDiffs() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "b.txt", "b"); + + String changesetsUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.changesets.href"); + + String diffUrl = given() + .when() + .get(changesetsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.changesets[0]._links.diff.href"); + + given() + .when() + .get(diffUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body() + .asString() + .contains("diff"); + + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindFileHistory() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a"); + repositoryGetRequest + .usingRepositoryResponse() + .requestSources() + .usingSourcesResponse() + .requestSelf("folder") + .usingSourcesResponse() + .requestSelf("subfolder") + .usingSourcesResponse() + .requestFileHistory("a.txt") + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .assertChangesets(changesets -> { + assertThat(changesets).hasSize(1); + assertThat(changesets.get(0)).containsEntry("id", changeset.getId()); + assertThat(changesets.get(0)).containsEntry("description", changeset.getDescription()); + } + ); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java b/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java new file mode 100644 index 0000000000..41fa2a6a3d --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java @@ -0,0 +1,249 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import io.restassured.response.Response; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + + +/** + * Encapsulate rest requests of a repository in builder pattern + *

+ * A Get Request can be applied with the methods request*() + * These methods return a AppliedGet*Request object + * This object can be used to apply general assertions over the rest Assured response + * In the AppliedGet*Request classes there is a using*Response() method + * that return the *Response class containing specific operations related to the specific response + * the *Response class contains also the request*() method to apply the next GET request from a link in the response. + */ +public class RepositoryRequests { + + private String url; + private String username; + private String password; + + static RepositoryRequests start() { + return new RepositoryRequests(); + } + + Given given() { + return new Given(); + } + + + /** + * Apply a GET Request to the extracted url from the given link + * + * @param linkPropertyName the property name of link + * @param response the response containing the link + * @return the response of the GET request using the given link + */ + private Response getResponseFromLink(Response response, String linkPropertyName) { + return getResponse(response + .then() + .extract() + .path(linkPropertyName)); + } + + + /** + * Apply a GET Request to the given url and return the response. + * + * @param url the url of the GET request + * @return the response of the GET request using the given url + */ + private Response getResponse(String url) { + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .get(url); + } + + private void setUrl(String url) { + this.url = url; + } + + private void setUsername(String username) { + this.username = username; + } + + private void setPassword(String password) { + this.password = password; + } + + private String getUrl() { + return url; + } + + private String getUsername() { + return username; + } + + private String getPassword() { + return password; + } + + class Given { + + GivenUrl url(String url) { + setUrl(url); + return new GivenUrl(); + } + + } + + class GivenWithUrlAndAuth { + AppliedRepositoryGetRequest get() { + return new AppliedRepositoryGetRequest( + getResponse(url) + ); + } + } + + class AppliedGetRequest { + private Response response; + + public AppliedGetRequest(Response response) { + this.response = response; + } + + /** + * apply custom assertions to the actual response + * + * @param consumer consume the response in order to assert the content. the header, the payload etc.. + * @return the self object + */ + SELF assertResponse(Consumer consumer) { + consumer.accept(response); + return (SELF) this; + } + + /** + * special assertion of the status code + * + * @param expectedStatusCode the expected status code + * @return the self object + */ + SELF assertStatusCode(int expectedStatusCode) { + this.response.then().assertThat().statusCode(expectedStatusCode); + return (SELF) this; + } + + } + + class AppliedRepositoryGetRequest extends AppliedGetRequest { + + AppliedRepositoryGetRequest(Response response) { + super(response); + } + + RepositoryResponse usingRepositoryResponse() { + return new RepositoryResponse(super.response); + } + } + + class RepositoryResponse { + + private Response repositoryResponse; + + public RepositoryResponse(Response repositoryResponse) { + this.repositoryResponse = repositoryResponse; + } + + AppliedGetSourcesRequest requestSources() { + return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href")); + } + + AppliedGetChangesetsRequest requestChangesets(String fileName) { + return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href")); + } + } + + class AppliedGetChangesetsRequest extends AppliedGetRequest { + + AppliedGetChangesetsRequest(Response response) { + super(response); + } + + ChangesetsResponse usingChangesetsResponse() { + return new ChangesetsResponse(super.response); + } + } + + class ChangesetsResponse { + private Response changesetsResponse; + + public ChangesetsResponse(Response changesetsResponse) { + this.changesetsResponse = changesetsResponse; + } + + ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) { + List changesets = changesetsResponse.then().extract().path("_embedded.changesets"); + changesetsConsumer.accept(changesets); + return this; + } + + AppliedGetDiffRequest requestDiff(String revision) { + return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); + } + + } + + class AppliedGetSourcesRequest extends AppliedGetRequest { + + public AppliedGetSourcesRequest(Response sourcesResponse) { + super(sourcesResponse); + } + + SourcesResponse usingSourcesResponse() { + return new SourcesResponse(super.response); + } + } + + class SourcesResponse { + + private Response sourcesResponse; + + SourcesResponse(Response sourcesResponse) { + this.sourcesResponse = sourcesResponse; + } + + SourcesResponse assertRevision(Consumer assertRevision) { + String revision = sourcesResponse.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + SourcesResponse assertFiles(Consumer assertFiles) { + List files = sourcesResponse.then().extract().path("files"); + assertFiles.accept(files); + return this; + } + + AppliedGetChangesetsRequest requestFileHistory(String fileName) { + return new AppliedGetChangesetsRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href")); + } + + AppliedGetSourcesRequest requestSelf(String fileName) { + return new AppliedGetSourcesRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href")); + } + } + + class AppliedGetDiffRequest extends AppliedGetRequest { + + AppliedGetDiffRequest(Response response) { + super(response); + } + } + + class GivenUrl { + + GivenWithUrlAndAuth usernameAndPassword(String username, String password) { + setUsername(username); + setPassword(password); + return new GivenWithUrlAndAuth(); + } + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index 442856eefa..0e2eb24ca9 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -43,6 +43,7 @@ public class RepositoryUtil { static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { File file = new File(repositoryClient.getWorkingCopy(), fileName); + Files.createParentDirs(file); Files.write(content, file, Charsets.UTF_8); addWithParentDirectories(repositoryClient, file); return commit(repositoryClient, username, "added " + fileName); @@ -53,7 +54,6 @@ public class RepositoryUtil { String thisName = file.getName(); String path; if (!repositoryClient.getWorkingCopy().equals(parent)) { - addWithParentDirectories(repositoryClient, parent); path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; } else { path = thisName; diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 7fc8484d75..93ec098597 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index 56d3e0ffe2..702e28711f 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -737,9 +737,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1867,7 +1868,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3285,7 +3286,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4733,6 +4747,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4955,6 +4979,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6402,7 +6430,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6857,6 +6885,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -7033,6 +7068,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index e6de568588..c5907d38bc 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js index 27bc10808e..ce10ec293c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { Image } from "@scm-manager/ui-components"; +import {Image} from "@scm-manager/ui-components"; type Props = { }; @@ -8,7 +8,7 @@ type Props = { class HgAvatar extends React.Component { render() { - return Mercurial Logo; + return Mercurial Logo; } } diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index 251b5241a2..8822bd5e57 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -671,9 +671,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1801,7 +1802,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3217,7 +3218,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4636,6 +4650,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4868,6 +4892,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6283,7 +6311,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6739,6 +6767,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6915,6 +6950,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 98f0394833..118108c882 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js index 06fc2454db..731c9ddf33 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { Image } from "@scm-manager/ui-components"; +import {Image} from "@scm-manager/ui-components"; type Props = { }; @@ -8,7 +8,7 @@ type Props = { class SvnAvatar extends React.Component { render() { - return Subversion Logo; + return Subversion Logo; } } diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index 251b5241a2..8822bd5e57 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -671,9 +671,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1801,7 +1802,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3217,7 +3218,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4636,6 +4650,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4868,6 +4892,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6283,7 +6311,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6739,6 +6767,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6915,6 +6950,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 1b8eeb517e..e515002728 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -12,7 +12,7 @@ "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13", + "@scm-manager/ui-bundler": "^0.0.15", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index e0d20269fd..bab2b75068 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -728,9 +728,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -758,9 +758,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1983,7 +1984,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3605,7 +3606,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -5264,6 +5278,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -5506,6 +5530,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -7143,7 +7171,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -7627,6 +7655,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -7839,6 +7874,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index e9db94b9a6..9c06fc7740 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -14,7 +14,7 @@ "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" }, "browserify": { "transform": [ diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index 6c18546f36..24213220d9 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -737,9 +737,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1856,7 +1857,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3252,7 +3253,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4693,6 +4707,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4915,6 +4939,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6327,7 +6355,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6778,6 +6806,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6954,6 +6989,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui/package.json b/scm-ui/package.json index a531cb4bd9..50ae39cd29 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -28,13 +28,13 @@ "redux-thunk": "^2.3.0" }, "scripts": { - "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/styles/webfonts", - "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles", - "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles --watch --recursive", - "start-js": "ui-bundler serve --vendor vendor.bundle.js", + "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/scm-ui/styles/webfonts", + "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles", + "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles --watch --recursive", + "start-js": "ui-bundler serve --target target/scm-ui --vendor vendor.bundle.js", "start": "npm-run-all -p webfonts watch-css start-js", - "build-js": "ui-bundler bundle target/scm-ui.bundle.js", - "build-vendor": "ui-bundler vendor target/vendor.bundle.js", + "build-js": "ui-bundler bundle --mode=production target/scm-ui/scm-ui.bundle.js", + "build-vendor": "ui-bundler vendor --mode=production target/scm-ui/vendor.bundle.js", "build": "npm-run-all -s webfonts build-css build-vendor build-js", "test": "ui-bundler test", "test-ci": "ui-bundler test --ci", @@ -42,7 +42,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13", + "@scm-manager/ui-bundler": "^0.0.15", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml index abe87e697f..5e3ca2b6f5 100644 --- a/scm-ui/pom.xml +++ b/scm-ui/pom.xml @@ -10,9 +10,9 @@ 2.0.0-SNAPSHOT - sonia.scm.clients + sonia.scm scm-ui - pom + war 2.0.0-SNAPSHOT scm-ui @@ -26,6 +26,7 @@ + scm-ui @@ -93,6 +94,20 @@ + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + + + public + + + + + diff --git a/scm-ui/public/index.html b/scm-ui/public/index.mustache similarity index 56% rename from scm-ui/public/index.html rename to scm-ui/public/index.mustache index 23dc1b9782..802be2ca97 100644 --- a/scm-ui/public/index.html +++ b/scm-ui/public/index.mustache @@ -8,20 +8,11 @@ manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - - - - - - + + SCM-Manager @@ -41,9 +32,9 @@ To create a production bundle, use `npm run build` or `yarn build`. --> - - + + diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 078a841ea2..26bfec8070 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -732,9 +732,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -762,9 +762,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -2042,7 +2043,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3743,7 +3744,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -5474,6 +5488,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -5743,6 +5767,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -7586,7 +7614,7 @@ source-map@^0.4.2, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -8120,6 +8148,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -8336,6 +8371,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index fb5cc57e44..da01814152 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -237,6 +237,12 @@ ${mustache.version} + + com.github.sdorra + web-resources + 1.0.2 + + com.github.sdorra spotter-core @@ -515,7 +521,7 @@ sonia.scm.ui.proxy - http://localhost:3000 + ${livereload.proxy} @@ -546,7 +552,42 @@ - + + + livereload + + + + livereload + + + + + http://localhost:3000 + + + + + ui-overlay + + + + !livereload + + + + + + + sonia.scm + scm-ui + 2.0.0-SNAPSHOT + war + + + + + release diff --git a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java deleted file mode 100644 index 0b80f158f3..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java +++ /dev/null @@ -1,24 +0,0 @@ -package sonia.scm; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * This dispatcher forwards every request to the index.html of the application. - * - * @since 2.0.0 - */ -public class ForwardingPushStateDispatcher implements PushStateDispatcher { - @Override - public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { - RequestDispatcher dispatcher = request.getRequestDispatcher("/index.html"); - try { - dispatcher.forward(request, response); - } catch (ServletException e) { - throw new IOException("failed to forward request", e); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java index 653f7b4bdc..f0d2807497 100644 --- a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java @@ -3,12 +3,13 @@ package sonia.scm; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import javax.inject.Inject; import javax.inject.Provider; /** * Injection Provider for the {@link PushStateDispatcher}. The provider will return a {@link ProxyPushStateDispatcher} * if the system property {@code PushStateDispatcherProvider#PROPERTY_TARGET} is set to a proxy target url, otherwise - * a {@link ForwardingPushStateDispatcher} is used. + * a {@link TemplatingPushStateDispatcher} is used. * * @since 2.0.0 */ @@ -17,11 +18,18 @@ public class PushStateDispatcherProvider implements Provider templatingPushStateDispatcherProvider; + + @Inject + public PushStateDispatcherProvider(Provider templatingPushStateDispatcherProvider) { + this.templatingPushStateDispatcherProvider = templatingPushStateDispatcherProvider; + } + @Override public PushStateDispatcher get() { String target = System.getProperty(PROPERTY_TARGET); if (Strings.isNullOrEmpty(target)) { - return new ForwardingPushStateDispatcher(); + return templatingPushStateDispatcherProvider.get(); } return new ProxyPushStateDispatcher(target); } diff --git a/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java new file mode 100644 index 0000000000..6652975c4a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java @@ -0,0 +1,61 @@ +package sonia.scm; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; + +/** + * This dispatcher renders the /index.mustache template, which is merged in from the scm-ui package. + * + * @since 2.0.0 + */ +public class TemplatingPushStateDispatcher implements PushStateDispatcher { + + @VisibleForTesting + static final String TEMPLATE = "/index.mustache"; + + private final TemplateEngine templateEngine; + + @Inject + public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory) { + this(templateEngineFactory.getDefaultEngine()); + } + + @VisibleForTesting + TemplatingPushStateDispatcher(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + @Override + public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + Template template = templateEngine.getTemplate(TEMPLATE); + try (Writer writer = response.getWriter()) { + template.execute(writer, new IndexHtmlModel(request)); + } + } + + @VisibleForTesting + static class IndexHtmlModel { + + private final HttpServletRequest request; + + private IndexHtmlModel(HttpServletRequest request) { + this.request = request; + } + + public String getContextPath() { + return request.getContextPath(); + } + + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java index 764e4f18d2..98117851db 100644 --- a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java @@ -1,7 +1,7 @@ package sonia.scm; +import com.github.sdorra.webresources.WebResourceSender; import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.filter.WebElement; @@ -15,7 +15,6 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; /** @@ -27,6 +26,7 @@ import java.net.URL; @WebElement(value = WebResourceServlet.PATTERN, regex = true) public class WebResourceServlet extends HttpServlet { + /** * exclude api requests and the old frontend servlets. * @@ -37,6 +37,11 @@ public class WebResourceServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class); + private final WebResourceSender sender = WebResourceSender.create() + .withGZIP() + .withGZIPMinLength(512) + .withBufferSize(16384); + private final UberWebResourceLoader webResourceLoader; private final PushStateDispatcher pushStateDispatcher; @@ -53,7 +58,7 @@ public class WebResourceServlet extends HttpServlet { LOG.trace("try to load {}", uri); URL url = webResourceLoader.getResource(uri); if (url != null) { - serveResource(response, url); + serveResource(request, response, url); } else { dispatch(request, response, uri); } @@ -72,10 +77,9 @@ public class WebResourceServlet extends HttpServlet { return HttpUtil.getStrippedURI(request); } - private void serveResource(HttpServletResponse response, URL url) { - // TODO lastModifiedDate, if-... ??? - try (OutputStream output = response.getOutputStream()) { - Resources.copy(url, output); + private void serveResource(HttpServletRequest request, HttpServletResponse response, URL url) { + try { + sender.resource(url).send(request, response); } catch (IOException ex) { LOG.warn("failed to serve resource: {}", url); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CRLFInjectionExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CRLFInjectionExceptionMapper.java new file mode 100644 index 0000000000..f4e8d3aa3c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CRLFInjectionExceptionMapper.java @@ -0,0 +1,13 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.util.CRLFInjectionException; + +import javax.ws.rs.core.Response; + +public class CRLFInjectionExceptionMapper extends StatusExceptionMapper { + + public CRLFInjectionExceptionMapper() { + super(CRLFInjectionException.class, Response.Status.BAD_REQUEST); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java index fcc4085486..3af3a1d15a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java @@ -6,11 +6,12 @@ import sonia.scm.repository.Repository; import javax.inject.Inject; import java.util.Optional; +import java.util.function.Supplier; public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; - private final ResourceLinks resourceLinks; + protected final ResourceLinks resourceLinks; @Inject public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { @@ -20,10 +21,14 @@ public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper pageResult, Repository repository) { - return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository)); } - private String createSelfLink(Repository repository) { + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, Supplier selfLinkSupplier) { + return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + } + + protected String createSelfLink(Repository repository) { return resourceLinks.changeset().all(repository.getNamespace(), repository.getName()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index 176a86dda7..99a996b02f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -1,27 +1,71 @@ package sonia.scm.api.v2.resources; -import javax.ws.rs.DefaultValue; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import sonia.scm.NotFoundException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; public class DiffRootResource { + public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; + private final RepositoryServiceFactory serviceFactory; - @GET - @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); + @Inject + public DiffRootResource(RepositoryServiceFactory serviceFactory) { + this.serviceFactory = serviceFactory; } + + /** + * Get the repository diff of a revision + * + * @param namespace repository namespace + * @param name repository name + * @param revision the revision + * @return the dif of the revision + * @throws NotFoundException if the repository is not found + */ @GET - @Path("{id}") - public Response get(String id) { - throw new UnsupportedOperationException(); + @Path("{revision}") + @Produces(VndMediaType.DIFF) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "Bad Request"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the diff"), + @ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException { + HttpUtil.checkForCRLFInjection(revision); + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + StreamingOutput responseEntry = output -> { + try { + repositoryService.getDiffCommand() + .setRevision(revision) + .retriveContent(output); + } catch (RevisionNotFoundException e) { + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + }; + return Response.ok(responseEntry) + .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision))) + .build(); + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java new file mode 100644 index 0000000000..692b2f57b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java @@ -0,0 +1,24 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapper { + + + @Inject + public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super(changesetToChangesetDtoMapper, resourceLinks); + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String revision, String path) { + return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path)); + } + + protected String createSelfLink(Repository repository, String revision, String path) { + return super.resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java new file mode 100644 index 0000000000..118cc4167a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -0,0 +1,92 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.io.IOException; + +@Slf4j +public class FileHistoryRootResource { + + private final RepositoryServiceFactory serviceFactory; + + private final FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + + @Inject + public FileHistoryRootResource(RepositoryServiceFactory serviceFactory, FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper) { + this.serviceFactory = serviceFactory; + this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper; + } + + /** + * Get all changesets related to the given file starting with the given revision + * + * @param namespace the repository namespace + * @param name the repository name + * @param revision the revision + * @param path the path of the file + * @param page pagination + * @param pageSize pagination + * @return all changesets related to the given file starting with the given revision + * @throws IOException on io error + * @throws RevisionNotFoundException on missing revision + * @throws RepositoryNotFoundException on missing repository + */ + @GET + @Path("{revision}/{path: .*}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + log.info("Get changesets of the file {} and revision {}", path, revision); + Repository repository = repositoryService.getRepository(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setPagingStart(page) + .setPagingLimit(pageSize) + .setPath(path) + .setStartChangeset(revision) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build(); + } else { + String message = String.format("for the revision %s and the file %s there is no changesets", revision, path); + log.error(message); + throw new InternalRepositoryException(message); + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index fdcc5c56ca..01085958f8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -11,6 +11,8 @@ import sonia.scm.repository.SubRepository; import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; + @Mapper public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { @@ -29,6 +31,7 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper permissionRootResource; private final Provider diffRootResource; private final Provider modificationsRootResource; + private final Provider fileHistoryRootResource; @Inject public RepositoryResource( @@ -51,7 +52,10 @@ public class RepositoryResource { Provider changesetRootResource, Provider sourceRootResource, Provider contentResource, Provider permissionRootResource, - Provider diffRootResource, Provider modificationsRootResource) { + Provider diffRootResource, + Provider modificationsRootResource, + Provider fileHistoryRootResource + ) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -64,6 +68,7 @@ public class RepositoryResource { this.permissionRootResource = permissionRootResource; this.diffRootResource = diffRootResource; this.modificationsRootResource = modificationsRootResource; + this.fileHistoryRootResource = fileHistoryRootResource; } /** @@ -167,6 +172,11 @@ public class RepositoryResource { return changesetRootResource.get(); } + @Path("history/") + public FileHistoryRootResource history() { + return fileHistoryRootResource.get(); + } + @Path("sources/") public SourceRootResource sources() { return sourceRootResource.get(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index e406c7a17c..2e7202e981 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -15,6 +15,10 @@ class ResourceLinks { this.uriInfoStore = uriInfoStore; } + // we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F' + private static String addPath(String sourceWithPath, String path) { + return URI.create(sourceWithPath).resolve(path).toASCIIString(); + } GroupLinks group() { return new GroupLinks(uriInfoStore.get()); @@ -322,6 +326,23 @@ class ResourceLinks { } } + public FileHistoryLinks fileHistory() { + return new FileHistoryLinks(uriInfoStore.get()); + } + + static class FileHistoryLinks { + private final LinkBuilder fileHistoryLinkBuilder; + + FileHistoryLinks(UriInfo uriInfo) { + fileHistoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, FileHistoryRootResource.class); + } + + String self(String namespace, String name, String changesetId, String path) { + return addPath(fileHistoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("history").parameters().method("getAll").parameters(changesetId, "").href(), path); + } + + } + public SourceLinks source() { return new SourceLinks(uriInfoStore.get()); } @@ -353,10 +374,7 @@ class ResourceLinks { return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path); } - // we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F' - private String addPath(String sourceWithPath, String path) { - return URI.create(sourceWithPath).resolve(path).toASCIIString(); - } + } public PermissionLinks permission() { return new PermissionLinks(uriInfoStore.get()); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java index 230cfa6c7a..4b9e502c4d 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java @@ -31,18 +31,11 @@ package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; - import java.net.MalformedURLException; import java.net.URL; - import java.nio.file.Files; import java.nio.file.Path; @@ -55,47 +48,27 @@ import java.nio.file.Path; public class PathWebResourceLoader implements WebResourceLoader { - /** Field description */ - private static final String DEFAULT_SEPARATOR = "/"; + private static final String SEPARATOR = "/"; /** * the logger for PathWebResourceLoader */ - private static final Logger logger = + private static final Logger LOG = LoggerFactory.getLogger(PathWebResourceLoader.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param directory - */ public PathWebResourceLoader(Path directory) { this.directory = directory; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * - * @return - */ @Override - public URL getResource(String path) - { + public URL getResource(String path) { URL resource = null; Path file = directory.resolve(filePath(path)); if (Files.exists(file) && ! Files.isDirectory(file)) { - logger.trace("found path {} at {}", path, file); + LOG.trace("found path {} at {}", path, file); try { @@ -103,56 +76,20 @@ public class PathWebResourceLoader implements WebResourceLoader } catch (MalformedURLException ex) { - logger.error("could not transform path to url", ex); + LOG.error("could not transform path to url", ex); } + } else { + LOG.trace("could not find file {}", file); } return resource; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * - * @return - */ - private String filePath(String path) - { - - // TODO handle illegal path parts, such as .. - String filePath = filePath(DEFAULT_SEPARATOR, path); - - if (!DEFAULT_SEPARATOR.equals(File.separator)) - { - filePath = filePath(File.separator, path); + private String filePath(String path) { + if (path.startsWith(SEPARATOR)) { + return path.substring(1); } - - return filePath; - } - - /** - * Method description - * - * - * @param separator - * @param path - * - * @return - */ - private String filePath(String separator, String path) - { - String filePath = path; - - if (filePath.startsWith(separator)) - { - filePath = filePath.substring(separator.length()); - } - - return filePath; + return path; } //~--- fields --------------------------------------------------------------- diff --git a/scm-webapp/src/main/webapp/index.html b/scm-webapp/src/main/webapp/index.html deleted file mode 100644 index e149a39434..0000000000 --- a/scm-webapp/src/main/webapp/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - diff --git a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java deleted file mode 100644 index e96464ee98..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package sonia.scm; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import java.io.IOException; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class ForwardingPushStateDispatcherTest { - - @Mock - private HttpServletRequest request; - - @Mock - private RequestDispatcher requestDispatcher; - - @Mock - private HttpServletResponse response; - - private ForwardingPushStateDispatcher dispatcher = new ForwardingPushStateDispatcher(); - - @Test - public void testDispatch() throws ServletException, IOException { - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - - dispatcher.dispatch(request, response, "/something"); - - verify(requestDispatcher).forward(request, response); - } - - @Test(expected = IOException.class) - public void testWrapServletException() throws ServletException, IOException { - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - doThrow(ServletException.class).when(requestDispatcher).forward(request, response); - - dispatcher.dispatch(request, response, "/something"); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java index 31e5f7c6dc..4316d9bc06 100644 --- a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java @@ -1,14 +1,23 @@ package sonia.scm; +import com.google.inject.util.Providers; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.TemplateEngine; -import static org.junit.Assert.*; - +@RunWith(MockitoJUnitRunner.class) public class PushStateDispatcherProviderTest { - private PushStateDispatcherProvider provider = new PushStateDispatcherProvider(); + @Mock + private TemplateEngine templateEngine; + + private PushStateDispatcherProvider provider = new PushStateDispatcherProvider( + Providers.of(new TemplatingPushStateDispatcher(templateEngine)) + ); @Test public void testGetProxyPushStateWithPropertySet() { @@ -20,7 +29,7 @@ public class PushStateDispatcherProviderTest { @Test public void testGetProxyPushStateWithoutProperty() { PushStateDispatcher dispatcher = provider.get(); - Assertions.assertThat(dispatcher).isInstanceOf(ForwardingPushStateDispatcher.class); + Assertions.assertThat(dispatcher).isInstanceOf(TemplatingPushStateDispatcher.class); } @After diff --git a/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java new file mode 100644 index 0000000000..126ba9ac0f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java @@ -0,0 +1,66 @@ +package sonia.scm; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TemplatingPushStateDispatcherTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private TemplateEngine templateEngine; + + @Mock + private Template template; + + private TemplatingPushStateDispatcher dispatcher; + + @Before + public void setUpMocks() { + dispatcher = new TemplatingPushStateDispatcher(templateEngine); + } + + @Test + public void testDispatch() throws IOException { + when(request.getContextPath()).thenReturn("/scm"); + when(templateEngine.getTemplate(TemplatingPushStateDispatcher.TEMPLATE)).thenReturn(template); + + when(response.getWriter()).thenReturn(new PrintWriter(new StringWriter())); + + dispatcher.dispatch(request, response, "/someurl"); + + verify(response).setContentType("text/html"); + verify(response).setCharacterEncoding("UTF-8"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + + verify(template).execute(any(Writer.class), captor.capture()); + + TemplatingPushStateDispatcher.IndexHtmlModel model = (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue(); + assertEquals("/scm", model.getContextPath()); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java index fa39239d5d..7da9c7b263 100644 --- a/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java @@ -19,10 +19,17 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class WebResourceServletTest { @Rule diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BaseRepositoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BaseRepositoryTest.java new file mode 100644 index 0000000000..a219681ea5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BaseRepositoryTest.java @@ -0,0 +1,42 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Provider; + +public class BaseRepositoryTest { + + + protected RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + protected RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + protected RepositoryManager manager; + protected Provider tagRootResource; + protected Provider branchRootResource; + protected Provider changesetRootResource; + protected Provider sourceRootResource; + protected Provider contentResource; + protected Provider permissionRootResource; + protected Provider diffRootResource; + protected Provider modificationsRootResource; + protected Provider fileHistoryRootResource; + protected Provider repositoryCollectionResource; + + + RepositoryRootResource getRepositoryRootResource() { + return new RepositoryRootResource(MockProvider.of(new RepositoryResource( + repositoryToDtoMapper, + dtoToRepositoryMapper, + manager, + tagRootResource, + branchRootResource, + changesetRootResource, + sourceRootResource, + contentResource, + permissionRootResource, + diffRootResource, + modificationsRootResource, + fileHistoryRootResource)), repositoryCollectionResource); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 0a209e905f..e0e851c581 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -44,7 +44,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j -public class BranchRootResourceTest { +public class BranchRootResourceTest extends BaseRepositoryTest { public static final String BRANCH_PATH = "space/repo/branches/master"; public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH; @@ -92,9 +92,8 @@ public class BranchRootResourceTest { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); - + super.branchRootResource = MockProvider.of(branchRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index 4ea08b4102..9adb53976c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -44,7 +44,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j -public class ChangesetRootResourceTest { +public class ChangesetRootResourceTest extends BaseRepositoryTest{ public static final String CHANGESET_PATH = "space/repo/changesets/"; @@ -79,10 +79,8 @@ public class ChangesetRootResourceTest { public void prepareEnvironment() throws Exception { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, - MockProvider.of(changesetRootResource), null, null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.changesetRootResource = MockProvider.of(changesetRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); @@ -125,7 +123,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } @Test @@ -155,7 +152,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java new file mode 100644 index 0000000000..020a099fe9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -0,0 +1,147 @@ +package sonia.scm.api.v2.resources; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.DiffCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URISyntaxException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class DiffResourceTest extends BaseRepositoryTest { + + + public static final String DIFF_PATH = "space/repo/diff/"; + public static final String DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + DIFF_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService service; + + @Mock + private DiffCommandBuilder diffCommandBuilder; + + private DiffRootResource diffRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + diffRootResource = new DiffRootResource(serviceFactory); + super.diffRootResource = MockProvider.of(diffRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class); + when(service.getDiffCommand()).thenReturn(diffCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGetDiffs() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retriveContent(any())).thenReturn(diffCommandBuilder); + + MockHttpRequest request = MockHttpRequest + .get(DIFF_URL + "revision") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertThat(response.getStatus()) + .isEqualTo(200); + assertThat(response.getContentAsString()) + .isNotNull(); + String expectedHeader = "Content-Disposition"; + String expectedValue = "attachment; filename=\"repo-revision.diff\"; filename*=utf-8''repo-revision.diff"; + assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue(); + assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0)) + .contains(expectedValue); + } + + @Test + public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + MockHttpRequest request = MockHttpRequest + .get(DIFF_URL + "revision") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet404OnMissingRevision() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); + + MockHttpRequest request = MockHttpRequest + .get(DIFF_URL + "revision") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet400OnCrlfInjection() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retriveContent(any())).thenThrow(RevisionNotFoundException.class); + + MockHttpRequest request = MockHttpRequest + .get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(400, response.getStatus()); + } + + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index 4d4ed435df..3505d00dc6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -14,6 +14,7 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); return dispatcher; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java new file mode 100644 index 0000000000..7dcf2acbd3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -0,0 +1,201 @@ +package sonia.scm.api.v2.resources; + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.LogCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class FileHistoryResourceTest extends BaseRepositoryTest { + + public static final String FILE_HISTORY_PATH = "space/repo/history/"; + public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService service; + + @Mock + private LogCommandBuilder logCommandBuilder; + + private FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + @InjectMocks + private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + + private FileHistoryRootResource fileHistoryRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); + fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); + super.fileHistoryRootResource = MockProvider.of(fileHistoryRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + when(service.getLogCommand()).thenReturn(logCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGetFileHistory() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + + @Test + public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + "revision/a.txt") + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet404OnMissingRevision() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(RevisionNotFoundException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet500OnInternalRepositoryException() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(InternalRepositoryException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } + + @Test + public void shouldGet500OnNullChangesets() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(null); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java index a93543bb2c..7dfcda2578 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java @@ -39,7 +39,7 @@ import static org.mockito.Mockito.when; @Slf4j @RunWith(MockitoJUnitRunner.Silent.class) -public class ModificationsResourceTest { +public class ModificationsResourceTest extends BaseRepositoryTest { public static final String MODIFICATIONS_PATH = "space/repo/modifications/"; @@ -71,10 +71,8 @@ public class ModificationsResourceTest { @Before public void prepareEnvironment() throws Exception { modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, - null, null, null, null, null, MockProvider.of(modificationsRootResource))), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.modificationsRootResource = MockProvider.of(modificationsRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 3f2ea9b317..d25cfe0a71 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -64,7 +64,7 @@ import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class PermissionRootResourceTest { +public class PermissionRootResourceTest extends BaseRepositoryTest { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; @@ -137,9 +137,8 @@ public class PermissionRootResourceTest { initMocks(this); permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource), null, null)), null); - dispatcher = createDispatcher(repositoryRootResource); + super.permissionRootResource = MockProvider.of(permissionRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); subjectThreadState.bind(); ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 48ca62089f..fcc3c50058 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -55,7 +55,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class RepositoryRootResourceTest { +public class RepositoryRootResourceTest extends BaseRepositoryTest { private Dispatcher dispatcher; @@ -79,11 +79,12 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null, null, null); + super.repositoryToDtoMapper = repositoryToDtoMapper; + super.dtoToRepositoryMapper = dtoToRepositoryMapper; + super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); - dispatcher = createDispatcher(repositoryRootResource); + super.repositoryCollectionResource = MockProvider.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks)); + dispatcher = createDispatcher(getRepositoryRootResource()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 5b1f1490a3..1d0fac68e3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -22,6 +22,7 @@ public class ResourceLinksMock { when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); + when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 5f35ce9cf2..855ae2a861 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -32,7 +32,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @RunWith(MockitoJUnitRunner.Silent.class) -public class SourceRootResourceTest { +public class SourceRootResourceTest extends BaseRepositoryTest { private Dispatcher dispatcher; private final URI baseUri = URI.create("/"); @@ -63,20 +63,8 @@ public class SourceRootResourceTest { when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto); SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper); - RepositoryRootResource repositoryRootResource = - new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, - null, - null, - null, - null, - null, - MockProvider.of(sourceRootResource), - null, - null, - null, - null)), - null); - dispatcher = createDispatcher(repositoryRootResource); + super.sourceRootResource = MockProvider.of(sourceRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index ad4b396101..ec225b782b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.After; @@ -35,14 +34,15 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @Slf4j @RunWith(MockitoJUnitRunner.Silent.class) -public class TagRootResourceTest { +public class TagRootResourceTest extends BaseRepositoryTest { public static final String TAG_PATH = "space/repo/tags/"; public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH; - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + private Dispatcher dispatcher ; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -72,10 +72,8 @@ public class TagRootResourceTest { public void prepareEnvironment() throws Exception { tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, MockProvider.of(tagRootResource), null, - null, null, null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.tagRootResource = MockProvider.of(tagRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));