diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index b50c64b321..95deebfd60 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -75,6 +75,11 @@ public class ScmConfiguration implements Configuration { public static final String DEFAULT_PLUGINURL = "http://download.scm-manager.org/api/v2/plugins.json?os={os}&arch={arch}&snapshot=false&version={version}"; + /** + * Default url for login information (plugin and feature tips on the login page). + */ + public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info"; + /** * Default plugin url from version 1.0 */ @@ -177,6 +182,9 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "namespace-strategy") private String namespaceStrategy = "UsernameNamespaceStrategy"; + @XmlElement(name = "login-info-url") + private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL; + /** * Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)} @@ -216,6 +224,7 @@ public class ScmConfiguration implements Configuration { this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; this.namespaceStrategy = other.namespaceStrategy; + this.loginInfoUrl = other.loginInfoUrl; } /** @@ -350,6 +359,9 @@ public class ScmConfiguration implements Configuration { return namespaceStrategy; } + public String getLoginInfoUrl() { + return loginInfoUrl; + } /** * Returns true if failed authenticators are skipped. @@ -477,6 +489,10 @@ public class ScmConfiguration implements Configuration { this.namespaceStrategy = namespaceStrategy; } + public void setLoginInfoUrl(String loginInfoUrl) { + this.loginInfoUrl = loginInfoUrl; + } + @Override // Only for permission checks, don't serialize to XML @XmlTransient diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index f4b19d1e85..6870c85dea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -59,7 +59,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.BLAME, Command.BROWSE, Command.CAT, - Command.DIFF, + Command.DIFF, + Command.DIFF_RESULT, Command.LOG, Command.TAGS, Command.BRANCHES, @@ -168,6 +169,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitDiffCommand(context, repository); } + @Override + public DiffResultCommand getDiffResultCommand() { + return new GitDiffResultCommand(context, repository); + } + /** * Method description * diff --git a/scm-ui-components/packages/ui-types/src/Config.js b/scm-ui-components/packages/ui-types/src/Config.js index 0e82076848..5a9522585f 100644 --- a/scm-ui-components/packages/ui-types/src/Config.js +++ b/scm-ui-components/packages/ui-types/src/Config.js @@ -21,5 +21,6 @@ export type Config = { loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, namespaceStrategy: string, + loginInfoUrl: string, _links: Links }; diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 3964863f46..cdbedd8b6b 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -5,7 +5,11 @@ "logo-alt": "SCM-Manager", "username-placeholder": "Benutzername", "password-placeholder": "Passwort", - "submit": "Anmelden" + "submit": "Anmelden", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tipp", + "loading": "Lade Daten ..." }, "logout": { "error": { diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 20977583ac..a94965fa68 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -43,7 +43,8 @@ "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", - "namespace-strategy": "Namespace Strategie" + "namespace-strategy": "Namespace Strategie", + "login-info-url": "Login Info URL" }, "validation": { "date-format-invalid": "Das Datumsformat ist ungültig", @@ -73,6 +74,7 @@ "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", - "nameSpaceStrategyHelpText": "Strategie für Namespaces." + "nameSpaceStrategyHelpText": "Strategie für Namespaces.", + "loginInfoUrlHelpText": "URL zu der Login Information (Plugin und Feature Tipps auf der Login Seite). Um die Login Information zu deaktivieren, kann das Feld leer gelassen werden." } } diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index c57ef700ef..181fdc975c 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -5,7 +5,12 @@ "logo-alt": "SCM-Manager", "username-placeholder": "Your Username", "password-placeholder": "Your Password", - "submit": "Login" + "submit": "Login", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tip", + "loading": "Loading ...", + "error": "Error" }, "logout": { "error": { diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 1aa13a3150..894d0563ba 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -43,7 +43,8 @@ "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "Enabled XSRF Protection", - "namespace-strategy": "Namespace Strategy" + "namespace-strategy": "Namespace Strategy", + "login-info-url": "Login Info URL" }, "validation": { "date-format-invalid": "The date format is not valid", @@ -73,6 +74,7 @@ "proxyUserHelpText": "The username for the proxy server authentication.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", - "nameSpaceStrategyHelpText": "The namespace strategy." + "nameSpaceStrategyHelpText": "The namespace strategy.", + "loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page). If this is omitted, no login information will be displayed." } } diff --git a/scm-ui/src/admin/components/form/ConfigForm.js b/scm-ui/src/admin/components/form/ConfigForm.js index 20e2db2599..25a24c4d28 100644 --- a/scm-ui/src/admin/components/form/ConfigForm.js +++ b/scm-ui/src/admin/components/form/ConfigForm.js @@ -54,6 +54,7 @@ class ConfigForm extends React.Component { loginAttemptLimitTimeout: 0, enabledXsrfProtection: true, namespaceStrategy: "", + loginInfoUrl: "", _links: {} }, showNotification: false, @@ -119,6 +120,7 @@ class ConfigForm extends React.Component { {noPermissionNotification} { const { t, realmDescription, + loginInfoUrl, enabledXsrfProtection, namespaceStrategy, hasUpdatePermission, @@ -57,6 +59,15 @@ class GeneralSettings extends React.Component {
+
+ +
{ ); } + handleLoginInfoUrlChange = (value: string) => { + this.props.onChange(true, value, "loginInfoUrl"); + }; + handleRealmDescriptionChange = (value: string) => { this.props.onChange(true, value, "realmDescription"); }; diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js new file mode 100644 index 0000000000..f6fc170826 --- /dev/null +++ b/scm-ui/src/components/InfoBox.js @@ -0,0 +1,83 @@ +//@flow +import * as React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; +import type { InfoItem } from "./InfoItem"; + +const styles = { + image: { + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", + width: 160, + height: 160 + }, + icon: { + color: "#bff1e6" + }, + label: { + marginTop: "0.5em" + }, + content: { + marginLeft: "1.5em" + }, + link: { + display: "block", + marginBottom: "1.5rem" + } +}; + +type Props = { + type: "plugin" | "feature", + item: InfoItem, + + // context props + classes: any, + t: string => string +}; + +class InfoBox extends React.Component { + + renderBody = () => { + const { item, t } = this.props; + + const bodyClasses = classNames("media-content", "content", this.props.classes.content); + const title = item ? item.title : t("login.loading"); + const summary = item ? item.summary : t("login.loading"); + + return ( +
+

{title}

+

{summary}

+
+ ); + + }; + + render() { + const { item, type, classes, t } = this.props; + const icon = type === "plugin" ? "puzzle-piece" : "star"; + return ( + +
+
+
+ +
{t("login." + type)}
+
{t("login.tip")}
+
+
+ {this.renderBody()} +
+
+ ); + } + +} + +export default injectSheet(styles)(translate("commons")(InfoBox)); + + diff --git a/scm-ui/src/components/InfoItem.js b/scm-ui/src/components/InfoItem.js new file mode 100644 index 0000000000..b947bd3fce --- /dev/null +++ b/scm-ui/src/components/InfoItem.js @@ -0,0 +1,8 @@ +// @flow +import type { Link } from "@scm-manager/ui-types"; + +export type InfoItem = { + title: string, + summary: string, + _links: {[string]: Link} +}; diff --git a/scm-ui/src/components/LoginForm.js b/scm-ui/src/components/LoginForm.js new file mode 100644 index 0000000000..95d4c0b7d3 --- /dev/null +++ b/scm-ui/src/components/LoginForm.js @@ -0,0 +1,120 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import { Image, ErrorNotification, InputField, SubmitButton, UnauthorizedError } from "@scm-manager/ui-components"; +import classNames from "classnames"; +import injectSheet from "react-jss"; + +const styles = { + avatar: { + marginTop: "-70px", + paddingBottom: "20px" + }, + avatarImage: { + border: "1px solid lightgray", + padding: "5px", + background: "#fff", + borderRadius: "50%", + width: "128px", + height: "128px" + }, + avatarSpacing: { + marginTop: "5rem" + } +}; + +type Props = { + error?: Error, + loading: boolean, + loginHandler: (username: string, password: string) => void, + + // context props + t: string => string, + classes: any +}; + +type State = { + username: string, + password: string +}; + +class LoginForm extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { username: "", password: "" }; + } + + handleSubmit = (event: Event) => { + event.preventDefault(); + if (this.isValid()) { + this.props.loginHandler( + this.state.username, + this.state.password + ); + } + }; + + handleUsernameChange = (value: string) => { + this.setState({ username: value }); + }; + + handlePasswordChange = (value: string) => { + this.setState({ password: value }); + }; + + isValid() { + return this.state.username && this.state.password; + } + + areCredentialsInvalid() { + const { t, error } = this.props; + if (error instanceof UnauthorizedError) { + return new Error(t("errorNotification.wrongLoginCredentials")); + } else { + return error; + } + } + + render() { + const { loading, classes, t } = this.props; + return ( +
+

{t("login.title")}

+

{t("login.subtitle")}

+
+
+ {t("login.logo-alt")} +
+ +
+ + + + +
+
+ ); + } + +} + +export default injectSheet(styles)(translate("commons")(LoginForm)); + + diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js new file mode 100644 index 0000000000..3f7ea5ede3 --- /dev/null +++ b/scm-ui/src/components/LoginInfo.js @@ -0,0 +1,97 @@ +//@flow +import React from "react"; +import InfoBox from "./InfoBox"; +import type { InfoItem } from "./InfoItem"; +import LoginForm from "./LoginForm"; +import { Loading } from "@scm-manager/ui-components"; + +type Props = { + loginInfoLink?: string, + loading?: boolean, + error?: Error, + loginHandler: (username: string, password: string) => void, +}; + +type LoginInfoResponse = { + plugin?: InfoItem, + feature?: InfoItem +}; + +type State = { + info?: LoginInfoResponse, + loading?: boolean, +}; + +class LoginInfo extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + loading: !!props.loginInfoLink + }; + } + + fetchLoginInfo = (url: string) => { + return fetch(url) + .then(response => response.json()) + .then(info => { + this.setState({ + info, + loading: false + }); + }); + }; + + timeout = (ms: number, promise: Promise) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("timeout during fetch of login info")); + }, ms); + promise.then(resolve, reject); + }); + }; + + componentDidMount() { + const { loginInfoLink } = this.props; + if (!loginInfoLink) { + return; + } + this.timeout(1000, this.fetchLoginInfo(loginInfoLink)) + .catch(() => { + this.setState({ + loading: false + }); + }); + } + + createInfoPanel = (info: LoginInfoResponse) => ( +
+ + +
+ ); + + render() { + const { info, loading } = this.state; + if (loading) { + return ; + } + + let infoPanel; + if (info) { + infoPanel = this.createInfoPanel(info); + } + + return ( + <> + + {infoPanel} + + ); + } + +} + +export default LoginInfo; + + diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 0f29013392..f8246ab88b 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,147 +1,71 @@ //@flow import React from "react"; -import {Redirect, withRouter} from "react-router-dom"; -import injectSheet from "react-jss"; -import {translate} from "react-i18next"; -import {getLoginFailure, isAuthenticated, isLoginPending, login} from "../modules/auth"; -import {connect} from "react-redux"; - -import {ErrorNotification, Image, InputField, SubmitButton, UnauthorizedError} from "@scm-manager/ui-components"; +import { Redirect, withRouter } from "react-router-dom"; +import { + login, + isAuthenticated, + isLoginPending, + getLoginFailure +} from "../modules/auth"; +import { connect } from "react-redux"; +import { getLoginLink, getLoginInfoLink } from "../modules/indexResource"; +import LoginInfo from "../components/LoginInfo"; import classNames from "classnames"; -import {getLoginLink} from "../modules/indexResource"; +import injectSheet from "react-jss"; const styles = { - avatar: { - marginTop: "-70px", - paddingBottom: "20px" - }, - avatarImage: { - border: "1px solid lightgray", - padding: "5px", - background: "#fff", - borderRadius: "50%", - width: "128px", - height: "128px" - }, - avatarSpacing: { - marginTop: "5rem" + section: { + paddingTop: "2em" } }; type Props = { authenticated: boolean, loading: boolean, - error: Error, + error?: Error, link: string, + loginInfoLink?: string, // dispatcher props login: (link: string, username: string, password: string) => void, // context props - t: string => string, classes: any, + t: string => string, from: any, location: any }; -type State = { - username: string, - password: string -}; +class Login extends React.Component { -class Login extends React.Component { - constructor(props: Props) { - super(props); - this.state = { username: "", password: "" }; - } - - handleUsernameChange = (value: string) => { - this.setState({ username: value }); + handleLogin = (username: string, password: string): void => { + const { link, login } = this.props; + login(link, username, password); }; - handlePasswordChange = (value: string) => { - this.setState({ password: value }); - }; - - handleSubmit = (event: Event) => { - event.preventDefault(); - if (this.isValid()) { - this.props.login( - this.props.link, - this.state.username, - this.state.password - ); - } - }; - - isValid() { - return this.state.username && this.state.password; - } - - isInValid() { - return !this.isValid(); - } - - areCredentialsInvalid() { - const { t, error } = this.props; - if (error instanceof UnauthorizedError) { - return new Error(t("errorNotification.wrongLoginCredentials")); - } else { - return error; - } - } - renderRedirect = () => { const { from } = this.props.location.state || { from: { pathname: "/" } }; - return ; + return ; }; render() { - const { authenticated, loading, t, classes } = this.props; + const { authenticated, classes, ...restProps } = this.props; if (authenticated) { return this.renderRedirect(); } return ( -
+
-
-
-

{t("login.title")}

-

{t("login.subtitle")}

-
-
- {t("login.logo-alt")} -
- -
- - - - -
+
+
+
- ); + ); } } @@ -150,11 +74,13 @@ const mapStateToProps = state => { const loading = isLoginPending(state); const error = getLoginFailure(state); const link = getLoginLink(state); + const loginInfoLink = getLoginInfoLink(state); return { authenticated, loading, error, - link + link, + loginInfoLink }; }; @@ -169,6 +95,6 @@ const StyledLogin = injectSheet(styles)( connect( mapStateToProps, mapDispatchToProps - )(translate("commons")(Login)) + )(Login) ); export default withRouter(StyledLogin); diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index aa8ebad5a2..d62e6b8b5d 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -172,6 +172,10 @@ export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } +export function getLoginInfoLink(state: Object) { + return getLink(state, "loginInfo"); +} + export function getUserAutoCompleteLink(state: Object): string { const link = getLinkCollection(state, "autocomplete").find( i => i.name === "users" diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index ddb2a748e5..48d66e9768 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -526,5 +526,9 @@ form .field:not(.is-grouped) { } } +// cursor +.has-cursor-pointer { + cursor: pointer; +} @import "bulma-popover/css/bulma-popover"; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 1852d6fdc4..30d936d4c5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -32,6 +32,7 @@ public class ConfigDto extends HalRepresentation { private long loginAttemptLimitTimeout; private boolean enabledXsrfProtection; private String namespaceStrategy; + private String loginInfoUrl; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 906ff66759..d05596abd5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; @@ -7,6 +8,7 @@ import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.PluginPermissions; import sonia.scm.repository.RepositoryRolePermissions; @@ -23,11 +25,13 @@ public class IndexDtoGenerator extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final SCMContextProvider scmContextProvider; + private final ScmConfiguration configuration; @Inject - public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) { + public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) { this.resourceLinks = resourceLinks; this.scmContextProvider = scmContextProvider; + this.configuration = configuration; } public IndexDto generate() { @@ -36,6 +40,11 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.self(resourceLinks.index().self()); builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self())); + String loginInfoUrl = configuration.getLoginInfoUrl(); + if (!Strings.isNullOrEmpty(loginInfoUrl)) { + builder.single(link("loginInfo", loginInfoUrl)); + } + if (SecurityUtils.getSubject().isAuthenticated()) { builder.single( link("me", resourceLinks.me().self()), diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index dd09e50266..e7ae446185 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -50,6 +50,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertTrue(config.isEnabledXsrfProtection()); assertEquals("username", config.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl()); } private ConfigDto createDefaultDto() { @@ -73,6 +74,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setLoginAttemptLimitTimeout(40); configDto.setEnabledXsrfProtection(true); configDto.setNamespaceStrategy("username"); + configDto.setLoginInfoUrl("https://scm-manager.org/login-info"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java index 93099cf5ea..9dfa5fca28 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java @@ -3,9 +3,11 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import org.assertj.core.api.Assertions; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; import java.net.URI; import java.util.Optional; @@ -19,9 +21,22 @@ public class IndexResourceTest { @Rule public final ShiroRule shiroRule = new ShiroRule(); - private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class); - private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider); - private final IndexResource indexResource = new IndexResource(indexDtoGenerator); + private ScmConfiguration configuration; + private SCMContextProvider scmContextProvider; + private IndexResource indexResource; + + + @Before + public void setUpObjectUnderTest() { + this.configuration = new ScmConfiguration(); + this.scmContextProvider = mock(SCMContextProvider.class); + IndexDtoGenerator generator = new IndexDtoGenerator( + ResourceLinksMock.createMock(URI.create("/")), + scmContextProvider, + configuration + ); + this.indexResource = new IndexResource(generator); + } @Test public void shouldRenderLoginUrlsForUnauthenticatedRequest() { @@ -30,6 +45,22 @@ public class IndexResourceTest { Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); } + @Test + public void shouldRenderLoginInfoUrl() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent(); + } + + @Test + public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() { + configuration.setLoginInfoUrl(""); + + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent(); + } + @Test public void shouldRenderSelfLinkForUnauthenticatedRequest() { IndexDto index = indexResource.getIndex(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index ee940a9721..6ae6d5d2f1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -80,6 +80,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertTrue(dto.isEnabledXsrfProtection()); assertEquals("username", dto.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -118,6 +119,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setLoginAttemptLimitTimeout(2); config.setEnabledXsrfProtection(true); config.setNamespaceStrategy("username"); + config.setLoginInfoUrl("https://scm-manager.org/login-info"); return config; }