diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5421312a13..5c25740da4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
-### Fixed
+### Fixed
+- JWT token timeout is now handled properly ([#1297](https://github.com/scm-manager/scm-manager/pull/1297))
+- Fix text-overflow in danger zone ([#1298](https://github.com/scm-manager/scm-manager/pull/1298))
- Fix plugin installation error if previously a plugin was installed with the same dependency which is still pending. ([#1300](https://github.com/scm-manager/scm-manager/pull/1300))
## [2.4.0] - 2020-08-14
diff --git a/Jenkinsfile b/Jenkinsfile
index 688930d227..2ef5929e09 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -75,7 +75,9 @@ node('docker') {
integrationTest: {
stage('Integration Test') {
mvn 'verify -Pit -DskipUnitTests -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true'
- junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml'
+ junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/cypress-reports/TEST-*.xml'
+ archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/videos/*.mp4'
+ archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/screenshots/**/*.png'
}
}
)
@@ -190,7 +192,7 @@ node('docker') {
String mainBranch
Maven setupMavenBuild() {
- MavenWrapperInDocker mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.7_10")
+ MavenWrapperInDocker mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.8_10")
mvn.enableDockerHost = true
// disable logging durring the build
diff --git a/build/Dockerfile b/build/Dockerfile
index c9afa6992e..6ef5ff1cfb 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -22,7 +22,7 @@
# SOFTWARE.
#
-FROM adoptopenjdk/openjdk11:x86_64-debian-jdk-11.0.7_10
+FROM adoptopenjdk/openjdk11:x86_64-debian-jdk-11.0.8_10
ENV DOCKER_VERSION=19.03.8 \
DOCKER_CHANNEL=stable \
@@ -34,11 +34,24 @@ COPY modprobe.sh /usr/local/bin/modprobe
# install required packages
RUN set -eux; \
apt-get update \
- && apt-get install -y \
+ && apt-get install --no-install-recommends -y \
# mercurial is requried for integration tests of the scm-hg-plugin
mercurial \
# git is required by yarn install of scm-ui
git \
+ # the following dependencies are required for cypress tests and are copied from
+ # https://github.com/cypress-io/cypress-docker-images/blob/master/base/12.18.3/Dockerfile
+ libgtk2.0-0 \
+ libgtk-3-0 \
+ libnotify-dev \
+ libgconf-2-4 \
+ libgbm-dev \
+ libnss3 \
+ libxss1 \
+ libasound2 \
+ libxtst6 \
+ xauth \
+ xvfb \
# download docker
&& curl -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz \
&& echo "${DOCKER_CHECKSUM} docker-${DOCKER_VERSION}.tgz" > docker-${DOCKER_VERSION}.sha256sum \
diff --git a/build/Makefile b/build/Makefile
index 173df8bcb8..180b0db68d 100644
--- a/build/Makefile
+++ b/build/Makefile
@@ -1,4 +1,4 @@
-VERSION:=11.0.7_10
+VERSION:=11.0.8_10
.PHONY:build
build:
diff --git a/package.json b/package.json
index 61331899bc..4772263be5 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"build": "webpack --mode=production --config=scm-ui/ui-scripts/src/webpack.config.js",
"build:dev": "webpack --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
"test": "lerna run --scope '@scm-manager/ui-*' --scope '@scm-manager/eslint-config' test",
+ "e2e-tests": "lerna run --scope '@scm-manager/e2e-tests' ci",
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
"serve": "NODE_ENV=development webpack-dev-server --hot --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
"deploy": "ui-scripts publish",
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
index 04ffe12e21..a6514465fd 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
@@ -67,6 +67,7 @@ public class AuthenticationFilter extends HttpFilter {
*/
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
+
private final Set tokenGenerators;
protected ScmConfiguration configuration;
@@ -117,7 +118,7 @@ public class AuthenticationFilter extends HttpFilter {
}
/**
- * Sends status code 403 back to client, if the authentication has failed.
+ * Sends status code 401 back to client, if the authentication has failed.
* In all other cases the method will send status code 403 back to client.
*
* @param request servlet request
@@ -209,12 +210,8 @@ public class AuthenticationFilter extends HttpFilter {
subject.login(token);
processChain(request, response, chain, subject);
} catch (TokenExpiredException ex) {
- if (logger.isTraceEnabled()) {
- logger.trace("{} expired", token.getClass(), ex);
- } else {
- logger.debug("{} expired", token.getClass());
- }
- handleUnauthorized(request, response, chain);
+ // Rethrow to be caught by TokenExpiredFilter
+ throw ex;
} catch (AuthenticationException ex) {
logger.warn("authentication failed", ex);
handleUnauthorized(request, response, chain);
diff --git a/scm-it/pom.xml b/scm-it/pom.xml
index 44f11f82d2..6911304659 100644
--- a/scm-it/pom.xml
+++ b/scm-it/pom.xml
@@ -186,6 +186,36 @@
+
+ com.github.sdorra
+ buildfrontend-maven-plugin
+
+ ${basedir}/..
+
+ ${nodejs.version}
+
+
+ YARN
+ ${yarn.version}
+
+
+
+ true
+
+
+
+ e2e
+ integration-test
+
+ run
+
+
+
+
+
org.apache.maven.plugins
maven-dependency-plugin
@@ -284,7 +314,7 @@
DEVELOPMENT
${project.parent.build.directory}/scm-it
- ${project.basedir}/../scm-webapp/src/main/resources/logback.default.xml
+ ${project.basedir}/../scm-webapp/src/main/resources/logback.ci.xml
diff --git a/scm-ui/e2e-tests/cypress.json b/scm-ui/e2e-tests/cypress.json
index 03e8546581..128f39dcfb 100644
--- a/scm-ui/e2e-tests/cypress.json
+++ b/scm-ui/e2e-tests/cypress.json
@@ -1,3 +1,5 @@
{
- "baseUrl": "http://localhost:8081/scm"
+ "baseUrl": "http://localhost:8081/scm",
+ "videoUploadOnPasses": false,
+ "videoCompression": false
}
diff --git a/scm-ui/e2e-tests/cypress/.gitignore b/scm-ui/e2e-tests/cypress/.gitignore
new file mode 100644
index 0000000000..adaba54810
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/.gitignore
@@ -0,0 +1,2 @@
+videos
+screenshots
diff --git a/scm-ui/e2e-tests/package.json b/scm-ui/e2e-tests/package.json
index 91b2691a7e..d5348843d0 100644
--- a/scm-ui/e2e-tests/package.json
+++ b/scm-ui/e2e-tests/package.json
@@ -6,8 +6,16 @@
"author": "Eduard Heimbuch ",
"license": "MIT",
"private": false,
- "devDependencies": {
+ "scripts": {
+ "headless": "cypress run",
+ "ci": "node src/index.js"
+ },
+ "dependencies": {
+ "@ffmpeg-installer/ffmpeg": "^1.0.20",
"cypress": "^4.12.0",
+ "fluent-ffmpeg": "^2.1.2"
+ },
+ "devDependencies": {
"eslint-plugin-cypress": "^2.11.1"
},
"prettier": "@scm-manager/prettier-config",
diff --git a/scm-ui/e2e-tests/src/index.js b/scm-ui/e2e-tests/src/index.js
new file mode 100644
index 0000000000..55c08bb312
--- /dev/null
+++ b/scm-ui/e2e-tests/src/index.js
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+const cypress = require("cypress");
+const fs = require("fs");
+const path = require("path");
+const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path;
+const ffmpeg = require("fluent-ffmpeg");
+
+ffmpeg.setFfmpegPath(ffmpegPath);
+
+const options = {
+ reporter: "junit",
+ reporterOptions: {
+ mochaFile: path.join("..", "target", "cypress-reports", "TEST-[hash].xml")
+ }
+};
+
+const createOutputFile = test => {
+ const title = test.title.join(" -- ");
+ return path.join("cypress", "videos", `${title}.mp4`);
+};
+
+const cutVideo = (video, test) => {
+ return new Promise((resolve, reject) => {
+ const title = createOutputFile(test);
+ ffmpeg(video)
+ .setStartTime(test.videoTimestamp / 1000)
+ .setDuration(test.wallClockDuration / 1000)
+ .output(title)
+ .on("end", err => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ })
+ .on("error", err => {
+ reject(err);
+ })
+ .run();
+ });
+};
+
+cypress
+ .run(options)
+ .then(results => {
+ results.runs.forEach(run => {
+ // remove videos of successful runs
+ if (!run.shouldUploadVideo) {
+ fs.unlinkSync(run.video);
+ } else {
+ const cuts = [];
+ run.tests.forEach(test => {
+ if (test.state !== "passed") {
+ cuts.push(cutVideo(run.video, test));
+ }
+ });
+ Promise.all(cuts)
+ .then(() => fs.unlinkSync(run.video))
+ .catch(err => console.error("failed to cut video", err));
+ }
+ });
+ })
+ .catch(err => console.error(err));
diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts
index 346b02845d..a371b06571 100644
--- a/scm-ui/ui-components/src/apiclient.ts
+++ b/scm-ui/ui-components/src/apiclient.ts
@@ -23,7 +23,14 @@
*/
import { contextPath } from "./urls";
-import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors";
+import {
+ createBackendError,
+ ForbiddenError,
+ isBackendError,
+ UnauthorizedError,
+ BackendErrorContent,
+ TOKEN_EXPIRED_ERROR_CODE
+} from "./errors";
type SubscriptionEvent = {
type: string;
@@ -120,7 +127,13 @@ function handleFailure(response: Response) {
if (!response.ok) {
if (isBackendError(response)) {
return response.json().then((content: BackendErrorContent) => {
- throw createBackendError(content, response.status);
+ if (content.errorCode === TOKEN_EXPIRED_ERROR_CODE) {
+ window.location.replace(`${contextPath}/login`);
+ // Throw error because if redirect is not instantaneous, we want to display something senseful
+ throw new UnauthorizedError("Unauthorized", 401);
+ } else {
+ throw createBackendError(content, response.status);
+ }
});
} else {
if (response.status === 401) {
diff --git a/scm-ui/ui-components/src/devBuild.test.ts b/scm-ui/ui-components/src/devBuild.test.ts
index 8df47ec6ee..5964032f53 100644
--- a/scm-ui/ui-components/src/devBuild.test.ts
+++ b/scm-ui/ui-components/src/devBuild.test.ts
@@ -25,31 +25,37 @@
import { createAttributesForTesting, isDevBuild } from "./devBuild";
describe("devbuild tests", () => {
- let env: string | undefined;
+ let stage: string | undefined;
+
+ const setStage = (s?: string) => {
+ // @ts-ignore scmStage is set on the index page
+ window.scmStage = s;
+ };
beforeAll(() => {
- env = process.env.NODE_ENV;
+ // @ts-ignore scmStage is set on the index page
+ stage = window.scmStage;
});
afterAll(() => {
- process.env.NODE_ENV = env;
+ setStage(stage);
});
describe("isDevBuild tests", () => {
it("should return true for development", () => {
- process.env.NODE_ENV = "development";
+ setStage("development");
expect(isDevBuild()).toBe(true);
});
it("should return false for production", () => {
- process.env.NODE_ENV = "production";
+ setStage("production");
expect(isDevBuild()).toBe(false);
});
});
describe("createAttributesForTesting in non development mode", () => {
beforeAll(() => {
- process.env.NODE_ENV = "production";
+ setStage("production");
});
it("should return undefined for non development", () => {
@@ -60,7 +66,7 @@ describe("devbuild tests", () => {
describe("createAttributesForTesting in development mode", () => {
beforeAll(() => {
- process.env.NODE_ENV = "development";
+ setStage("development");
});
it("should return undefined for non development", () => {
diff --git a/scm-ui/ui-components/src/devBuild.ts b/scm-ui/ui-components/src/devBuild.ts
index abf1a18dc7..4425f695ab 100644
--- a/scm-ui/ui-components/src/devBuild.ts
+++ b/scm-ui/ui-components/src/devBuild.ts
@@ -22,7 +22,8 @@
* SOFTWARE.
*/
-export const isDevBuild = () => process.env.NODE_ENV === "development";
+// @ts-ignore scmStage is set on the index page
+export const isDevBuild = () => (window.scmStage || "").toUpperCase() === "DEVELOPMENT";
export const createAttributesForTesting = (testId?: string) => {
if (!testId || !isDevBuild()) {
diff --git a/scm-ui/ui-components/src/errors.ts b/scm-ui/ui-components/src/errors.ts
index 2d4757bc72..bf1f364e67 100644
--- a/scm-ui/ui-components/src/errors.ts
+++ b/scm-ui/ui-components/src/errors.ts
@@ -103,3 +103,5 @@ export function createBackendError(content: BackendErrorContent, statusCode: num
export function isBackendError(response: Response) {
return response.headers.get("Content-Type") === "application/vnd.scmm-error+json;v=2";
}
+
+export const TOKEN_EXPIRED_ERROR_CODE = "DDS8D8unr1";
diff --git a/scm-ui/ui-scripts/src/webpack.config.js b/scm-ui/ui-scripts/src/webpack.config.js
index f25e6c3122..0bc9f93fd7 100644
--- a/scm-ui/ui-scripts/src/webpack.config.js
+++ b/scm-ui/ui-scripts/src/webpack.config.js
@@ -22,12 +22,13 @@
* SOFTWARE.
*/
const path = require("path");
-const createIndexMiddleware = require("./middleware/IndexMiddleware");
-const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const WorkerPlugin = require("worker-plugin");
+const createIndexMiddleware = require("./middleware/IndexMiddleware");
+const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
+
const isDevelopment = process.env.NODE_ENV === "development";
const root = path.resolve(process.cwd(), "scm-ui");
@@ -39,6 +40,8 @@ let mode = "production";
if (isDevelopment) {
mode = "development";
babelPlugins.push(require.resolve("react-refresh/babel"));
+ // it is ok to use require here, because we want to load the package conditionally
+ // eslint-disable-next-line global-require
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
webpackPlugins.push(new ReactRefreshWebpackPlugin());
}
@@ -113,13 +116,15 @@ module.exports = [
historyApiFallback: true,
overlay: true,
port: 3000,
- before: function(app) {
+ before: app => {
app.use(createContextPathMiddleware("/scm"));
},
- after: function(app) {
+ after: app => {
const templatePath = path.join(root, "ui-webapp", "public", "index.mustache");
+ const stage = process.env.NODE_ENV || "DEVELOPMENT";
const renderParams = {
- contextPath: "/scm"
+ contextPath: "/scm",
+ scmStage: stage.toUpperCase()
};
app.use(createIndexMiddleware(templatePath, renderParams));
},
diff --git a/scm-ui/ui-webapp/public/index.mustache b/scm-ui/ui-webapp/public/index.mustache
index d94bc552ee..3c0a51713c 100644
--- a/scm-ui/ui-webapp/public/index.mustache
+++ b/scm-ui/ui-webapp/public/index.mustache
@@ -47,6 +47,7 @@
-->
diff --git a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx
index 5fb14e4557..3844717487 100644
--- a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx
@@ -36,9 +36,20 @@ type Props = {
};
const DangerZoneContainer = styled.div`
- padding: 1rem;
+ padding: 1.5rem 1rem;
border: 1px solid #ff6a88;
border-radius: 5px;
+ > .level {
+ flex-flow: wrap;
+
+ .level-left {
+ max-width: 100%;
+ }
+
+ .level-right {
+ margin-top: 0.75rem;
+ }
+ }
> *:not(:last-child) {
padding-bottom: 1.5rem;
border-bottom: solid 2px whitesmoke;
diff --git a/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx
index 8585e98466..23f6ea10a5 100644
--- a/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx
@@ -26,9 +26,8 @@ import { connect } from "react-redux";
import { compose } from "redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
-import { History } from "history";
import { Repository } from "@scm-manager/ui-types";
-import { confirmAlert, DeleteButton, ErrorNotification, Level, ButtonGroup } from "@scm-manager/ui-components";
+import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos";
type Props = RouteComponentProps &
@@ -89,10 +88,11 @@ class DeleteRepo extends React.Component {
+
{t("deleteRepo.subtitle")}
-
{t("deleteRepo.description")}
-
+
+ {t("deleteRepo.description")}
+
}
right={}
/>
diff --git a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx
index ede6414029..9254774a6d 100644
--- a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx
@@ -38,7 +38,7 @@ type Props = {
};
const RenameRepository: FC = ({ repository, indexLinks }) => {
- let history = useHistory();
+ const history = useHistory();
const [t] = useTranslation("repos");
const [error, setError] = useState(undefined);
const [loading, setLoading] = useState(false);
@@ -156,10 +156,11 @@ const RenameRepository: FC = ({ repository, indexLinks }) => {
/>
+
{t("renameRepo.subtitle")}
-
{t("renameRepo.description")}
-
+
+ {t("renameRepo.description")}
+
}
right={