mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-18 03:01:05 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -184,8 +184,8 @@ public class ScmConfiguration implements Configuration {
|
||||
@XmlElement(name = "xsrf-protection")
|
||||
private boolean enabledXsrfProtection = true;
|
||||
|
||||
@XmlElement(name = "default-namespace-strategy")
|
||||
private String defaultNamespaceStrategy = "sonia.scm.repository.DefaultNamespaceStrategy";
|
||||
@XmlElement(name = "namespace-strategy")
|
||||
private String namespaceStrategy = "UsernameNamespaceStrategy";
|
||||
|
||||
|
||||
/**
|
||||
@@ -227,7 +227,7 @@ public class ScmConfiguration implements Configuration {
|
||||
this.loginAttemptLimit = other.loginAttemptLimit;
|
||||
this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout;
|
||||
this.enabledXsrfProtection = other.enabledXsrfProtection;
|
||||
this.defaultNamespaceStrategy = other.defaultNamespaceStrategy;
|
||||
this.namespaceStrategy = other.namespaceStrategy;
|
||||
}
|
||||
|
||||
public Set<String> getAdminGroups() {
|
||||
@@ -366,8 +366,8 @@ public class ScmConfiguration implements Configuration {
|
||||
return loginAttemptLimit > 0;
|
||||
}
|
||||
|
||||
public String getDefaultNamespaceStrategy() {
|
||||
return defaultNamespaceStrategy;
|
||||
public String getNamespaceStrategy() {
|
||||
return namespaceStrategy;
|
||||
}
|
||||
|
||||
|
||||
@@ -501,8 +501,8 @@ public class ScmConfiguration implements Configuration {
|
||||
this.enabledXsrfProtection = enabledXsrfProtection;
|
||||
}
|
||||
|
||||
public void setDefaultNamespaceStrategy(String defaultNamespaceStrategy) {
|
||||
this.defaultNamespaceStrategy = defaultNamespaceStrategy;
|
||||
public void setNamespaceStrategy(String namespaceStrategy) {
|
||||
this.namespaceStrategy = namespaceStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -248,7 +248,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
/**
|
||||
* Returns true if the {@link Repository} is valid.
|
||||
* <ul>
|
||||
* <li>The name is not empty and contains only A-z, 0-9, _, -, /</li>
|
||||
* <li>The namespace is valid</li>
|
||||
* <li>The name is valid</li>
|
||||
* <li>The type is not empty</li>
|
||||
* <li>The contact is empty or contains a valid email address</li>
|
||||
* </ul>
|
||||
@@ -257,9 +258,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type)
|
||||
&& ((Util.isEmpty(contact))
|
||||
|| ValidationUtil.isMailAddressValid(contact));
|
||||
return ValidationUtil.isRepositoryNameValid(namespace)
|
||||
&& ValidationUtil.isRepositoryNameValid(name)
|
||||
&& Util.isNotEmpty(type)
|
||||
&& ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,14 +35,12 @@ package sonia.scm.util;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
import sonia.scm.Validateable;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -52,15 +50,16 @@ public final class ValidationUtil
|
||||
|
||||
/** Field description */
|
||||
private static final String REGEX_MAIL =
|
||||
"^[A-z0-9][\\w.-]*@[A-z0-9][\\w\\-\\.]*\\.[A-z0-9][A-z0-9-]+$";
|
||||
"^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$";
|
||||
|
||||
/** Field description */
|
||||
private static final String REGEX_NAME =
|
||||
"^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$";
|
||||
"^[A-Za-z0-9\\.\\-_@]|[^ ]([A-Za-z0-9\\.\\-_@ ]*[A-Za-z0-9\\.\\-_@]|[^ ])?$";
|
||||
|
||||
public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$";
|
||||
|
||||
/** Field description */
|
||||
private static final String REGEX_REPOSITORYNAME =
|
||||
"(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$";
|
||||
private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -142,37 +141,15 @@ public final class ValidationUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Returns {@code true} if the repository name is valid.
|
||||
*
|
||||
*
|
||||
* @param name
|
||||
* @param name repository name
|
||||
* @since 1.9
|
||||
*
|
||||
* @return
|
||||
* @return {@code true} if repository name is valid
|
||||
*/
|
||||
public static boolean isRepositoryNameValid(String name)
|
||||
{
|
||||
Pattern pattern = Pattern.compile(REGEX_REPOSITORYNAME);
|
||||
boolean result = true;
|
||||
|
||||
if (Util.isNotEmpty(name))
|
||||
{
|
||||
for (String p : Splitter.on('/').split(name))
|
||||
{
|
||||
if (!pattern.matcher(p).matches())
|
||||
{
|
||||
result = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
public static boolean isRepositoryNameValid(String name) {
|
||||
return PATTERN_REPOSITORYNAME.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,8 @@ public class VndMediaType {
|
||||
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
|
||||
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
|
||||
|
||||
public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX;
|
||||
|
||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||
public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;
|
||||
|
||||
@@ -143,51 +143,21 @@ public class ValidationUtilTest
|
||||
assertFalse(ValidationUtil.isNotContaining("test", "t"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testIsRepositoryNameValid()
|
||||
{
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid("scm"));
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid("scm/main"));
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid("scm/plugins/git-plugin"));
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid("s"));
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid("sc"));
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid(".scm/plugins"));
|
||||
|
||||
// issue 142
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("."));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("/"));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("scm/plugins/."));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("scm/../plugins"));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("scm/main/"));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("/scm/main/"));
|
||||
|
||||
// issue 144
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("scm/./main"));
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid("scm//main"));
|
||||
|
||||
// issue 148
|
||||
//J-
|
||||
public void testIsRepositoryNameValid() {
|
||||
String[] validPaths = {
|
||||
"scm",
|
||||
"scm/main",
|
||||
"scm/plugins/git-plugin",
|
||||
"s",
|
||||
"sc",
|
||||
".scm/plugins",
|
||||
".hiddenrepo",
|
||||
"b.",
|
||||
"...",
|
||||
"..c",
|
||||
"d..",
|
||||
"a/b..",
|
||||
"a/..b",
|
||||
"a..c",
|
||||
"a..c"
|
||||
};
|
||||
|
||||
|
||||
// issue 142, 144 and 148
|
||||
String[] invalidPaths = {
|
||||
".",
|
||||
"/",
|
||||
@@ -228,17 +198,22 @@ public class ValidationUtilTest
|
||||
"abc)abc",
|
||||
"abc[abc",
|
||||
"abc]abc",
|
||||
"abc|abc"
|
||||
"abc|abc",
|
||||
"scm/main",
|
||||
"scm/plugins/git-plugin",
|
||||
".scm/plugins",
|
||||
"a/b..",
|
||||
"a/..b",
|
||||
"scm/main",
|
||||
"scm/plugins/git-plugin",
|
||||
"scm/plugins/git-plugin"
|
||||
};
|
||||
//J+
|
||||
|
||||
for (String path : validPaths)
|
||||
{
|
||||
for (String path : validPaths) {
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid(path));
|
||||
}
|
||||
|
||||
for (String path : invalidPaths)
|
||||
{
|
||||
for (String path : invalidPaths) {
|
||||
assertFalse(ValidationUtil.isRepositoryNameValid(path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ export type Config = {
|
||||
pluginUrl: string,
|
||||
loginAttemptLimitTimeout: number,
|
||||
enabledXsrfProtection: boolean,
|
||||
defaultNamespaceStrategy: string,
|
||||
namespaceStrategy: string,
|
||||
_links: Links
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type NamespaceStrategies = {
|
||||
current: string,
|
||||
available: string[],
|
||||
_links: Links
|
||||
};
|
||||
@@ -26,3 +26,5 @@ export type { SubRepository, File } from "./Sources";
|
||||
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
|
||||
|
||||
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";
|
||||
|
||||
export type { NamespaceStrategies } from "./NamespaceStrategies";
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
|
||||
"plugin-url": "Plugin URL",
|
||||
"enabled-xsrf-protection": "XSRF Protection aktivieren",
|
||||
"default-namespace-strategy": "Default Namespace Strategie"
|
||||
"namespace-strategy": "Namespace Strategie"
|
||||
},
|
||||
"validation": {
|
||||
"date-format-invalid": "Das Datumsformat ist ungültig",
|
||||
@@ -87,6 +87,6 @@
|
||||
"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.",
|
||||
"defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces."
|
||||
"nameSpaceStrategyHelpText": "Strategie für Namespaces."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"repository": {
|
||||
"namespace": "Namespace",
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"contact": "Kontakt",
|
||||
@@ -8,10 +9,12 @@
|
||||
"lastModified": "Zuletzt bearbeitet"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||
"name-invalid": "Der Name des Repository ist ungültig",
|
||||
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein"
|
||||
},
|
||||
"help": {
|
||||
"namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.",
|
||||
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
|
||||
"typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).",
|
||||
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"skip-failed-authenticators": "Skip Failed Authenticators",
|
||||
"plugin-url": "Plugin URL",
|
||||
"enabled-xsrf-protection": "Enabled XSRF Protection",
|
||||
"default-namespace-strategy": "Default Namespace Strategy"
|
||||
"namespace-strategy": "Namespace Strategy"
|
||||
},
|
||||
"validation": {
|
||||
"date-format-invalid": "The date format is not valid",
|
||||
@@ -87,6 +87,6 @@
|
||||
"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.",
|
||||
"defaultNameSpaceStrategyHelpText": "The default namespace strategy."
|
||||
"nameSpaceStrategyHelpText": "The namespace strategy."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"repository": {
|
||||
"namespace": "Namespace",
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"contact": "Contact",
|
||||
@@ -8,10 +9,12 @@
|
||||
"lastModified": "Last Modified"
|
||||
},
|
||||
"validation": {
|
||||
"namespace-invalid": "The repository namespace is invalid",
|
||||
"name-invalid": "The repository name is invalid",
|
||||
"contact-invalid": "Contact must be a valid mail address"
|
||||
},
|
||||
"help": {
|
||||
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",
|
||||
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
|
||||
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
|
||||
"contactHelpText": "Email address of the person who is responsible for this repository.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { SubmitButton, Notification } from "@scm-manager/ui-components";
|
||||
import type { Config } from "@scm-manager/ui-types";
|
||||
import type { NamespaceStrategies, Config } from "@scm-manager/ui-types";
|
||||
import ProxySettings from "./ProxySettings";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import BaseUrlSettings from "./BaseUrlSettings";
|
||||
@@ -13,9 +13,11 @@ type Props = {
|
||||
submitForm: Config => void,
|
||||
config?: Config,
|
||||
loading?: boolean,
|
||||
t: string => string,
|
||||
configReadPermission: boolean,
|
||||
configUpdatePermission: boolean
|
||||
configUpdatePermission: boolean,
|
||||
namespaceStrategies?: NamespaceStrategies,
|
||||
// context props
|
||||
t: string => string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -54,7 +56,7 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
pluginUrl: "",
|
||||
loginAttemptLimitTimeout: 0,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "",
|
||||
namespaceStrategy: "",
|
||||
_links: {}
|
||||
},
|
||||
showNotification: false,
|
||||
@@ -88,6 +90,7 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
const {
|
||||
loading,
|
||||
t,
|
||||
namespaceStrategies,
|
||||
configReadPermission,
|
||||
configUpdatePermission
|
||||
} = this.props;
|
||||
@@ -118,6 +121,7 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
<form onSubmit={this.submit}>
|
||||
{noPermissionNotification}
|
||||
<GeneralSettings
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
realmDescription={config.realmDescription}
|
||||
enableRepositoryArchive={config.enableRepositoryArchive}
|
||||
disableGroupingGrid={config.disableGroupingGrid}
|
||||
@@ -126,7 +130,7 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
skipFailedAuthenticators={config.skipFailedAuthenticators}
|
||||
pluginUrl={config.pluginUrl}
|
||||
enabledXsrfProtection={config.enabledXsrfProtection}
|
||||
defaultNamespaceStrategy={config.defaultNamespaceStrategy}
|
||||
namespaceStrategy={config.namespaceStrategy}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox, InputField } from "@scm-manager/ui-components";
|
||||
import { Checkbox, InputField} from "@scm-manager/ui-components";
|
||||
import type { NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import NamespaceStrategySelect from "./NamespaceStrategySelect";
|
||||
|
||||
type Props = {
|
||||
realmDescription: string,
|
||||
@@ -12,13 +14,16 @@ type Props = {
|
||||
skipFailedAuthenticators: boolean,
|
||||
pluginUrl: string,
|
||||
enabledXsrfProtection: boolean,
|
||||
defaultNamespaceStrategy: string,
|
||||
t: string => string,
|
||||
namespaceStrategy: string,
|
||||
namespaceStrategies?: NamespaceStrategies,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
hasUpdatePermission: boolean,
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class GeneralSettings extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
@@ -30,8 +35,9 @@ class GeneralSettings extends React.Component<Props> {
|
||||
skipFailedAuthenticators,
|
||||
pluginUrl,
|
||||
enabledXsrfProtection,
|
||||
defaultNamespaceStrategy,
|
||||
hasUpdatePermission
|
||||
namespaceStrategy,
|
||||
hasUpdatePermission,
|
||||
namespaceStrategies
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -67,13 +73,14 @@ class GeneralSettings extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.default-namespace-strategy")}
|
||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||
value={defaultNamespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
||||
/>
|
||||
<NamespaceStrategySelect
|
||||
label={t("general-settings.namespace-strategy")}
|
||||
onChange={this.handleNamespaceStrategyChange}
|
||||
value={namespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
helpText={t("help.nameSpaceStrategyHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
@@ -146,19 +153,17 @@ class GeneralSettings extends React.Component<Props> {
|
||||
handleAnonymousAccessEnabledChange = (value: string) => {
|
||||
this.props.onChange(true, value, "anonymousAccessEnabled");
|
||||
};
|
||||
|
||||
handleSkipFailedAuthenticatorsChange = (value: string) => {
|
||||
this.props.onChange(true, value, "skipFailedAuthenticators");
|
||||
};
|
||||
handlePluginUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "pluginUrl");
|
||||
};
|
||||
|
||||
handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enabledXsrfProtection");
|
||||
};
|
||||
handleDefaultNamespaceStrategyChange = (value: string) => {
|
||||
this.props.onChange(true, value, "defaultNamespaceStrategy");
|
||||
handleNamespaceStrategyChange = (value: string) => {
|
||||
this.props.onChange(true, value, "namespaceStrategy");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
63
scm-ui/src/config/components/form/NamespaceStrategySelect.js
Normal file
63
scm-ui/src/config/components/form/NamespaceStrategySelect.js
Normal file
@@ -0,0 +1,63 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate, type TFunction } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
import type { NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
namespaceStrategies: NamespaceStrategies,
|
||||
label: string,
|
||||
value?: string,
|
||||
disabled?: boolean,
|
||||
helpText?: string,
|
||||
onChange: (value: string, name?: string) => void,
|
||||
// context props
|
||||
t: TFunction
|
||||
};
|
||||
|
||||
class NamespaceStrategySelect extends React.Component<Props> {
|
||||
createNamespaceOptions = () => {
|
||||
const { namespaceStrategies, t } = this.props;
|
||||
let available = [];
|
||||
if (namespaceStrategies && namespaceStrategies.available) {
|
||||
available = namespaceStrategies.available;
|
||||
}
|
||||
|
||||
return available.map(ns => {
|
||||
const key = "namespaceStrategies." + ns;
|
||||
let label = t(key);
|
||||
if (label === key) {
|
||||
label = ns;
|
||||
}
|
||||
return {
|
||||
value: ns,
|
||||
label: label
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
findSelected = () => {
|
||||
const { namespaceStrategies, value } = this.props;
|
||||
if (namespaceStrategies.available.indexOf(value) < 0) {
|
||||
return namespaceStrategies.current;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, helpText, disabled, onChange } = this.props;
|
||||
const nsOptions = this.createNamespaceOptions();
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
onChange={onChange}
|
||||
value={this.findSelected()}
|
||||
disabled={disabled}
|
||||
options={nsOptions}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(NamespaceStrategySelect);
|
||||
@@ -14,9 +14,15 @@ import {
|
||||
modifyConfigReset
|
||||
} from "../modules/config";
|
||||
import { connect } from "react-redux";
|
||||
import type { Config } from "@scm-manager/ui-types";
|
||||
import type { Config, NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import ConfigForm from "../components/form/ConfigForm";
|
||||
import { getConfigLink } from "../../modules/indexResource";
|
||||
import {
|
||||
fetchNamespaceStrategiesIfNeeded,
|
||||
getFetchNamespaceStrategiesFailure,
|
||||
getNamespaceStrategies,
|
||||
isFetchNamespaceStrategiesPending
|
||||
} from "../modules/namespaceStrategies";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
@@ -24,11 +30,13 @@ type Props = {
|
||||
config: Config,
|
||||
configUpdatePermission: boolean,
|
||||
configLink: string,
|
||||
namespaceStrategies?: NamespaceStrategies,
|
||||
|
||||
// dispatch functions
|
||||
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||
fetchConfig: (link: string) => void,
|
||||
configReset: void => void,
|
||||
fetchNamespaceStrategiesIfNeeded: void => void,
|
||||
|
||||
// context objects
|
||||
t: string => string
|
||||
@@ -51,6 +59,7 @@ class GlobalConfig extends React.Component<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.configReset();
|
||||
this.props.fetchNamespaceStrategiesIfNeeded();
|
||||
if (this.props.configLink) {
|
||||
this.props.fetchConfig(this.props.configLink);
|
||||
} else {
|
||||
@@ -103,7 +112,7 @@ class GlobalConfig extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
renderContent = () => {
|
||||
const { error, loading, config, configUpdatePermission } = this.props;
|
||||
const { error, loading, config, configUpdatePermission, namespaceStrategies } = this.props;
|
||||
const { configReadPermission } = this.state;
|
||||
if (!error) {
|
||||
return (
|
||||
@@ -113,6 +122,7 @@ class GlobalConfig extends React.Component<Props, State> {
|
||||
submitForm={config => this.modifyConfig(config)}
|
||||
config={config}
|
||||
loading={loading}
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
configUpdatePermission={configUpdatePermission}
|
||||
configReadPermission={configReadPermission}
|
||||
/>
|
||||
@@ -133,23 +143,33 @@ const mapDispatchToProps = dispatch => {
|
||||
},
|
||||
configReset: () => {
|
||||
dispatch(modifyConfigReset());
|
||||
},
|
||||
fetchNamespaceStrategiesIfNeeded: () => {
|
||||
dispatch(fetchNamespaceStrategiesIfNeeded());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const loading = isFetchConfigPending(state) || isModifyConfigPending(state);
|
||||
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||
const loading = isFetchConfigPending(state)
|
||||
|| isModifyConfigPending(state)
|
||||
|| isFetchNamespaceStrategiesPending(state);
|
||||
const error = getFetchConfigFailure(state)
|
||||
|| getModifyConfigFailure(state)
|
||||
|| getFetchNamespaceStrategiesFailure(state);
|
||||
|
||||
const config = getConfig(state);
|
||||
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||
const configLink = getConfigLink(state);
|
||||
const namespaceStrategies = getNamespaceStrategies(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
config,
|
||||
configUpdatePermission,
|
||||
configLink
|
||||
configLink,
|
||||
namespaceStrategies
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const config = {
|
||||
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||
loginAttemptLimitTimeout: 300,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
namespaceStrategy: "UsernameNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
@@ -79,7 +79,7 @@ const configWithNullValues = {
|
||||
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||
loginAttemptLimitTimeout: 300,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
namespaceStrategy: "UsernameNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
|
||||
115
scm-ui/src/config/modules/namespaceStrategies.js
Normal file
115
scm-ui/src/config/modules/namespaceStrategies.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
|
||||
import * as types from "../../modules/types";
|
||||
import type { Action, NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
import { MODIFY_CONFIG_SUCCESS } from "./config";
|
||||
|
||||
export const FETCH_NAMESPACESTRATEGIES_TYPES =
|
||||
"scm/config/FETCH_NAMESPACESTRATEGIES_TYPES";
|
||||
export const FETCH_NAMESPACESTRATEGIES_TYPES_PENDING = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
|
||||
types.PENDING_SUFFIX
|
||||
}`;
|
||||
export const FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
|
||||
types.SUCCESS_SUFFIX
|
||||
}`;
|
||||
export const FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
|
||||
types.FAILURE_SUFFIX
|
||||
}`;
|
||||
|
||||
export function fetchNamespaceStrategiesIfNeeded() {
|
||||
return function(dispatch: any, getState: () => Object) {
|
||||
const state = getState();
|
||||
if (shouldFetchNamespaceStrategies(state)) {
|
||||
return fetchNamespaceStrategies(
|
||||
dispatch,
|
||||
state.indexResources.links.namespaceStrategies.href
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fetchNamespaceStrategies(dispatch: any, url: string) {
|
||||
dispatch(fetchNamespaceStrategiesPending());
|
||||
return apiClient
|
||||
.get(url)
|
||||
.then(response => response.json())
|
||||
.then(namespaceStrategies => {
|
||||
dispatch(fetchNamespaceStrategiesSuccess(namespaceStrategies));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchNamespaceStrategiesFailure(error));
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldFetchNamespaceStrategies(state: Object) {
|
||||
if (
|
||||
isFetchNamespaceStrategiesPending(state) ||
|
||||
getFetchNamespaceStrategiesFailure(state)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !state.namespaceStrategies || !state.namespaceStrategies.current;
|
||||
}
|
||||
|
||||
export function fetchNamespaceStrategiesPending(): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespaceStrategiesSuccess(
|
||||
namespaceStrategies: NamespaceStrategies
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
|
||||
payload: namespaceStrategies
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespaceStrategiesFailure(error: Error): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
// reducers
|
||||
|
||||
export default function reducer(
|
||||
state: Object = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (
|
||||
action.type === FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS &&
|
||||
action.payload
|
||||
) {
|
||||
return action.payload;
|
||||
} else if (action.type === MODIFY_CONFIG_SUCCESS && action.payload) {
|
||||
const config = action.payload;
|
||||
return {
|
||||
...state,
|
||||
current: config.namespaceStrategy
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// selectors
|
||||
|
||||
export function getNamespaceStrategies(state: Object) {
|
||||
if (state.namespaceStrategies) {
|
||||
return state.namespaceStrategies;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function isFetchNamespaceStrategiesPending(state: Object) {
|
||||
return isPending(state, FETCH_NAMESPACESTRATEGIES_TYPES);
|
||||
}
|
||||
|
||||
export function getFetchNamespaceStrategiesFailure(state: Object) {
|
||||
return getFailure(state, FETCH_NAMESPACESTRATEGIES_TYPES);
|
||||
}
|
||||
199
scm-ui/src/config/modules/namespaceStrategies.test.js
Normal file
199
scm-ui/src/config/modules/namespaceStrategies.test.js
Normal file
@@ -0,0 +1,199 @@
|
||||
// @flow
|
||||
import fetchMock from "fetch-mock";
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import {
|
||||
FETCH_NAMESPACESTRATEGIES_TYPES,
|
||||
FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
|
||||
FETCH_NAMESPACESTRATEGIES_TYPES_PENDING,
|
||||
FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
|
||||
fetchNamespaceStrategiesIfNeeded,
|
||||
fetchNamespaceStrategiesSuccess,
|
||||
shouldFetchNamespaceStrategies,
|
||||
default as reducer,
|
||||
getNamespaceStrategies,
|
||||
isFetchNamespaceStrategiesPending,
|
||||
getFetchNamespaceStrategiesFailure
|
||||
} from "./namespaceStrategies";
|
||||
import { MODIFY_CONFIG_SUCCESS } from "./config";
|
||||
|
||||
const strategies = {
|
||||
current: "UsernameNamespaceStrategy",
|
||||
available: [
|
||||
"UsernameNamespaceStrategy",
|
||||
"CustomNamespaceStrategy",
|
||||
"CurrentYearNamespaceStrategy",
|
||||
"RepositoryTypeNamespaceStrategy"
|
||||
],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/scm/api/v2/namespaceStrategies"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("namespace strategy caching", () => {
|
||||
it("should fetch strategies, on empty state", () => {
|
||||
expect(shouldFetchNamespaceStrategies({})).toBe(true);
|
||||
});
|
||||
|
||||
it("should fetch strategies, on empty namespaceStrategies node", () => {
|
||||
const state = {
|
||||
namespaceStrategies: {}
|
||||
};
|
||||
expect(shouldFetchNamespaceStrategies(state)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not fetch strategies, on pending state", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
|
||||
}
|
||||
};
|
||||
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not fetch strategies, on failure state", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_NAMESPACESTRATEGIES_TYPES]: new Error("no...")
|
||||
}
|
||||
};
|
||||
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not fetch strategies, if they are already fetched", () => {
|
||||
const state = {
|
||||
namespaceStrategies: {
|
||||
current: "some"
|
||||
}
|
||||
};
|
||||
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("namespace strategies fetch", () => {
|
||||
const URL = "http://scm.hitchhiker.com/api/v2/namespaceStrategies";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
const createStore = (initialState = {}) => {
|
||||
return mockStore({
|
||||
...initialState,
|
||||
indexResources: {
|
||||
links: {
|
||||
namespaceStrategies: {
|
||||
href: URL
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
it("should successfully fetch strategies", () => {
|
||||
fetchMock.getOnce(URL, strategies);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING },
|
||||
{
|
||||
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
|
||||
payload: strategies
|
||||
}
|
||||
];
|
||||
|
||||
const store = createStore();
|
||||
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE on server error", () => {
|
||||
fetchMock.getOnce(URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = createStore();
|
||||
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_PENDING);
|
||||
expect(actions[1].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not dispatch any action, if the strategies are already fetched", () => {
|
||||
const store = createStore({
|
||||
namespaceStrategies: strategies
|
||||
});
|
||||
store.dispatch(fetchNamespaceStrategiesIfNeeded());
|
||||
expect(store.getActions().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("namespace strategies reducer", () => {
|
||||
it("should return unmodified state on unknown action", () => {
|
||||
const state = {};
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the strategies on success", () => {
|
||||
const newState = reducer({}, fetchNamespaceStrategiesSuccess(strategies));
|
||||
expect(newState).toBe(strategies);
|
||||
});
|
||||
|
||||
it("should clear store if config was modified", () => {
|
||||
const modifyConfigAction = {
|
||||
type: MODIFY_CONFIG_SUCCESS,
|
||||
payload: {
|
||||
namespaceStrategy: "CustomNamespaceStrategy"
|
||||
}
|
||||
};
|
||||
const newState = reducer(strategies, modifyConfigAction);
|
||||
expect(newState.current).toEqual("CustomNamespaceStrategy");
|
||||
});
|
||||
});
|
||||
|
||||
describe("namespace strategy selectors", () => {
|
||||
const error = new Error("The end of the universe");
|
||||
|
||||
it("should return an empty object", () => {
|
||||
expect(getNamespaceStrategies({})).toEqual({});
|
||||
});
|
||||
|
||||
it("should return the namespace strategies", () => {
|
||||
const state = {
|
||||
namespaceStrategies: strategies
|
||||
};
|
||||
expect(getNamespaceStrategies(state)).toBe(strategies);
|
||||
});
|
||||
|
||||
it("should return true, when fetch namespace strategies is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchNamespaceStrategiesPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch strategies is not pending", () => {
|
||||
expect(isFetchNamespaceStrategiesPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch namespace strategies did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_NAMESPACESTRATEGIES_TYPES]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchNamespaceStrategiesFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch strategies did not fail", () => {
|
||||
expect(getFetchNamespaceStrategiesFailure({})).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import permissions from "./repos/permissions/modules/permissions";
|
||||
import config from "./config/modules/config";
|
||||
import namespaceStrategies from "./config/modules/namespaceStrategies";
|
||||
import indexResources from "./modules/indexResource";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
@@ -38,7 +39,8 @@ function createReduxStore(history: BrowserHistory) {
|
||||
groups,
|
||||
auth,
|
||||
config,
|
||||
sources
|
||||
sources,
|
||||
namespaceStrategies
|
||||
});
|
||||
|
||||
return createStore(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SubmitButton,
|
||||
Textarea
|
||||
} from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import type { Repository, RepositoryType } from "@scm-manager/ui-types";
|
||||
import * as validator from "./repositoryValidation";
|
||||
|
||||
@@ -15,16 +16,20 @@ type Props = {
|
||||
submitForm: Repository => void,
|
||||
repository?: Repository,
|
||||
repositoryTypes: RepositoryType[],
|
||||
namespaceStrategy: string,
|
||||
loading?: boolean,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
repository: Repository,
|
||||
namespaceValidationError: boolean,
|
||||
nameValidationError: boolean,
|
||||
contactValidationError: boolean
|
||||
};
|
||||
|
||||
const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
|
||||
|
||||
class RepositoryForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -38,9 +43,9 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
description: "",
|
||||
_links: {}
|
||||
},
|
||||
namespaceValidationError: false,
|
||||
nameValidationError: false,
|
||||
contactValidationError: false,
|
||||
descriptionValidationError: false
|
||||
contactValidationError: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,11 +64,14 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
const repository = this.state.repository;
|
||||
const { namespaceStrategy } = this.props;
|
||||
const { repository } = this.state;
|
||||
return !(
|
||||
this.state.namespaceValidationError ||
|
||||
this.state.nameValidationError ||
|
||||
this.state.contactValidationError ||
|
||||
this.isFalsy(repository.name)
|
||||
this.isFalsy(repository.name) ||
|
||||
(namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && this.isFalsy(repository.namespace))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -127,6 +135,31 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
renderNamespaceField = () => {
|
||||
const { namespaceStrategy, t } = this.props;
|
||||
const repository = this.state.repository;
|
||||
const props = {
|
||||
label: t("repository.namespace"),
|
||||
helpText: t("help.namespaceHelpText"),
|
||||
value: repository ? repository.namespace : "",
|
||||
onChange: this.handleNamespaceChange,
|
||||
errorMessage: t("validation.namespace-invalid"),
|
||||
validationError: this.state.namespaceValidationError
|
||||
};
|
||||
|
||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
return <InputField {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExtensionPoint
|
||||
name="repos.create.namespace"
|
||||
props={props}
|
||||
renderAll={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderCreateOnlyFields() {
|
||||
if (!this.isCreateMode()) {
|
||||
return null;
|
||||
@@ -135,6 +168,7 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
const repository = this.state.repository;
|
||||
return (
|
||||
<>
|
||||
{this.renderNamespaceField()}
|
||||
<InputField
|
||||
label={t("repository.name")}
|
||||
onChange={this.handleNameChange}
|
||||
@@ -154,6 +188,13 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
handleNamespaceChange = (namespace: string) => {
|
||||
this.setState({
|
||||
namespaceValidationError: !validator.isNameValid(namespace),
|
||||
repository: { ...this.state.repository, namespace }
|
||||
});
|
||||
};
|
||||
|
||||
handleNameChange = (name: string) => {
|
||||
this.setState({
|
||||
nameValidationError: !validator.isNameValid(name),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// @flow
|
||||
import { validation } from "@scm-manager/ui-components";
|
||||
|
||||
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
|
||||
|
||||
export const isNameValid = (name: string) => {
|
||||
return validation.isNameValid(name);
|
||||
return nameRegex.test(name);
|
||||
};
|
||||
|
||||
export function isContactValid(mail: string) {
|
||||
|
||||
@@ -11,6 +11,81 @@ describe("repository name validation", () => {
|
||||
expect(validator.isNameValid("scm/manager")).toBe(false);
|
||||
expect(validator.isNameValid("scm/ma/nager")).toBe(false);
|
||||
});
|
||||
|
||||
it("should allow same names as the backend", () => {
|
||||
const validPaths = [
|
||||
"scm",
|
||||
"s",
|
||||
"sc",
|
||||
".hiddenrepo",
|
||||
"b.",
|
||||
"...",
|
||||
"..c",
|
||||
"d..",
|
||||
"a..c"
|
||||
];
|
||||
|
||||
validPaths.forEach((path) =>
|
||||
expect(validator.isNameValid(path)).toBe(true)
|
||||
);
|
||||
});
|
||||
|
||||
it("should deny same names as the backend", () => {
|
||||
const invalidPaths = [
|
||||
".",
|
||||
"/",
|
||||
"//",
|
||||
"..",
|
||||
"/.",
|
||||
"/..",
|
||||
"./",
|
||||
"../",
|
||||
"/../",
|
||||
"/./",
|
||||
"/...",
|
||||
"/abc",
|
||||
".../",
|
||||
"/sdf/",
|
||||
"asdf/",
|
||||
"./b",
|
||||
"scm/plugins/.",
|
||||
"scm/../plugins",
|
||||
"scm/main/",
|
||||
"/scm/main/",
|
||||
"scm/./main",
|
||||
"scm//main",
|
||||
"scm\\main",
|
||||
"scm/main-$HOME",
|
||||
"scm/main-${HOME}-home",
|
||||
"scm/main-%HOME-home",
|
||||
"scm/main-%HOME%-home",
|
||||
"abc$abc",
|
||||
"abc%abc",
|
||||
"abc<abc",
|
||||
"abc>abc",
|
||||
"abc#abc",
|
||||
"abc+abc",
|
||||
"abc{abc",
|
||||
"abc}abc",
|
||||
"abc(abc",
|
||||
"abc)abc",
|
||||
"abc[abc",
|
||||
"abc]abc",
|
||||
"abc|abc",
|
||||
"scm/main",
|
||||
"scm/plugins/git-plugin",
|
||||
".scm/plugins",
|
||||
"a/b..",
|
||||
"a/..b",
|
||||
"scm/main",
|
||||
"scm/plugins/git-plugin",
|
||||
"scm/plugins/git-plugin"
|
||||
];
|
||||
|
||||
invalidPaths.forEach((path) =>
|
||||
expect(validator.isNameValid(path)).toBe(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("repository contact validation", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { Page } from "@scm-manager/ui-components";
|
||||
import RepositoryForm from "../components/form";
|
||||
import type { Repository, RepositoryType } from "@scm-manager/ui-types";
|
||||
import type { Repository, RepositoryType, NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchRepositoryTypesIfNeeded,
|
||||
getFetchRepositoryTypesFailure,
|
||||
@@ -19,15 +19,21 @@ import {
|
||||
} from "../modules/repos";
|
||||
import type { History } from "history";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
import {
|
||||
fetchNamespaceStrategiesIfNeeded,
|
||||
getFetchNamespaceStrategiesFailure, getNamespaceStrategies, isFetchNamespaceStrategiesPending
|
||||
} from "../../config/modules/namespaceStrategies";
|
||||
|
||||
type Props = {
|
||||
repositoryTypes: RepositoryType[],
|
||||
typesLoading: boolean,
|
||||
namespaceStrategies: NamespaceStrategies,
|
||||
pageLoading: boolean,
|
||||
createLoading: boolean,
|
||||
error: Error,
|
||||
repoLink: string,
|
||||
|
||||
// dispatch functions
|
||||
fetchNamespaceStrategiesIfNeeded: () => void,
|
||||
fetchRepositoryTypesIfNeeded: () => void,
|
||||
createRepo: (
|
||||
link: string,
|
||||
@@ -45,6 +51,7 @@ class Create extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.resetForm();
|
||||
this.props.fetchRepositoryTypesIfNeeded();
|
||||
this.props.fetchNamespaceStrategiesIfNeeded();
|
||||
}
|
||||
|
||||
repoCreated = (repo: Repository) => {
|
||||
@@ -55,9 +62,10 @@ class Create extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
typesLoading,
|
||||
pageLoading,
|
||||
createLoading,
|
||||
repositoryTypes,
|
||||
namespaceStrategies,
|
||||
createRepo,
|
||||
error
|
||||
} = this.props;
|
||||
@@ -67,13 +75,14 @@ class Create extends React.Component<Props> {
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
subtitle={t("create.subtitle")}
|
||||
loading={typesLoading}
|
||||
loading={pageLoading}
|
||||
error={error}
|
||||
showContentOnError={true}
|
||||
>
|
||||
<RepositoryForm
|
||||
repositoryTypes={repositoryTypes}
|
||||
loading={createLoading}
|
||||
namespaceStrategy={namespaceStrategies.current}
|
||||
submitForm={repo => {
|
||||
createRepo(repoLink, repo, (repo: Repository) =>
|
||||
this.repoCreated(repo)
|
||||
@@ -87,14 +96,18 @@ class Create extends React.Component<Props> {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const repositoryTypes = getRepositoryTypes(state);
|
||||
const typesLoading = isFetchRepositoryTypesPending(state);
|
||||
const namespaceStrategies = getNamespaceStrategies(state);
|
||||
const pageLoading = isFetchRepositoryTypesPending(state)
|
||||
|| isFetchNamespaceStrategiesPending(state);
|
||||
const createLoading = isCreateRepoPending(state);
|
||||
const error =
|
||||
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state);
|
||||
const error = getFetchRepositoryTypesFailure(state)
|
||||
|| getCreateRepoFailure(state)
|
||||
|| getFetchNamespaceStrategiesFailure(state);
|
||||
const repoLink = getRepositoriesLink(state);
|
||||
return {
|
||||
repositoryTypes,
|
||||
typesLoading,
|
||||
namespaceStrategies,
|
||||
pageLoading,
|
||||
createLoading,
|
||||
error,
|
||||
repoLink
|
||||
@@ -106,6 +119,9 @@ const mapDispatchToProps = dispatch => {
|
||||
fetchRepositoryTypesIfNeeded: () => {
|
||||
dispatch(fetchRepositoryTypesIfNeeded());
|
||||
},
|
||||
fetchNamespaceStrategiesIfNeeded: () => {
|
||||
dispatch(fetchNamespaceStrategiesIfNeeded());
|
||||
},
|
||||
createRepo: (
|
||||
link: string,
|
||||
repository: Repository,
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ConfigDto extends HalRepresentation {
|
||||
private String pluginUrl;
|
||||
private long loginAttemptLimitTimeout;
|
||||
private boolean enabledXsrfProtection;
|
||||
private String defaultNamespaceStrategy;
|
||||
private String namespaceStrategy;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.NamespaceStrategyValidator;
|
||||
import sonia.scm.util.ScmConfigurationUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -27,12 +28,16 @@ public class ConfigResource {
|
||||
private final ConfigDtoToScmConfigurationMapper dtoToConfigMapper;
|
||||
private final ScmConfigurationToConfigDtoMapper configToDtoMapper;
|
||||
private final ScmConfiguration configuration;
|
||||
private final NamespaceStrategyValidator namespaceStrategyValidator;
|
||||
|
||||
@Inject
|
||||
public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, ScmConfiguration configuration) {
|
||||
public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper,
|
||||
ScmConfigurationToConfigDtoMapper configToDtoMapper,
|
||||
ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator) {
|
||||
this.dtoToConfigMapper = dtoToConfigMapper;
|
||||
this.configToDtoMapper = configToDtoMapper;
|
||||
this.configuration = configuration;
|
||||
this.namespaceStrategyValidator = namespaceStrategyValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +83,9 @@ public class ConfigResource {
|
||||
// But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later.
|
||||
ConfigurationPermissions.write(configuration).check();
|
||||
|
||||
// ensure the namespace strategy is valid
|
||||
namespaceStrategyValidator.check(configDto.getNamespaceStrategy());
|
||||
|
||||
ScmConfiguration config = dtoToConfigMapper.map(configDto);
|
||||
synchronized (ScmConfiguration.class) {
|
||||
configuration.load(config);
|
||||
|
||||
@@ -59,6 +59,9 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
builder.single(link("permissions", resourceLinks.permissions().self()));
|
||||
}
|
||||
builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self()));
|
||||
|
||||
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
|
||||
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public class NamespaceStrategiesDto extends HalRepresentation {
|
||||
|
||||
private String current;
|
||||
private List<String> available;
|
||||
|
||||
public NamespaceStrategiesDto(String current, List<String> available, Links links) {
|
||||
super(links);
|
||||
this.current = current;
|
||||
this.available = available;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* RESTFul WebService Endpoint for namespace strategies.
|
||||
*/
|
||||
@Path(NamespaceStrategyResource.PATH)
|
||||
public class NamespaceStrategyResource {
|
||||
|
||||
static final String PATH = "v2/namespaceStrategies";
|
||||
|
||||
private Set<NamespaceStrategy> namespaceStrategies;
|
||||
private Provider<NamespaceStrategy> namespaceStrategyProvider;
|
||||
|
||||
@Inject
|
||||
public NamespaceStrategyResource(Set<NamespaceStrategy> namespaceStrategies, Provider<NamespaceStrategy> namespaceStrategyProvider) {
|
||||
this.namespaceStrategies = namespaceStrategies;
|
||||
this.namespaceStrategyProvider = namespaceStrategyProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available namespace strategies and the current selected.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
*
|
||||
* @return available and current namespace strategies
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.NAMESPACE_STRATEGIES)
|
||||
public NamespaceStrategiesDto get(@Context UriInfo uriInfo) {
|
||||
String currentStrategy = strategyAsString(namespaceStrategyProvider.get());
|
||||
List<String> availableStrategies = collectStrategyNames();
|
||||
|
||||
return new NamespaceStrategiesDto(currentStrategy, availableStrategies, createLinks(uriInfo));
|
||||
}
|
||||
|
||||
private Links createLinks(@Context UriInfo uriInfo) {
|
||||
return Links.linkingTo().self(uriInfo.getAbsolutePath().toASCIIString()).build();
|
||||
}
|
||||
|
||||
private String strategyAsString(NamespaceStrategy namespaceStrategy) {
|
||||
return namespaceStrategy.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
private List<String> collectStrategyNames() {
|
||||
return namespaceStrategies.stream().map(this::strategyAsString).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Email;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import sonia.scm.util.ValidationUtil;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
import java.time.Instant;
|
||||
@@ -25,8 +26,9 @@ public class RepositoryDto extends HalRepresentation {
|
||||
private List<HealthCheckFailureDto> healthCheckFailures;
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Instant lastModified;
|
||||
// we could not validate the namespace, this must be done by the namespace strategy
|
||||
private String namespace;
|
||||
@Pattern(regexp = "^[A-z0-9\\-_]+$")
|
||||
@Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
|
||||
private String name;
|
||||
private boolean archived = false;
|
||||
@NotEmpty
|
||||
|
||||
@@ -277,6 +277,23 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
public NamespaceStrategiesLinks namespaceStrategies() {
|
||||
return new NamespaceStrategiesLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class NamespaceStrategiesLinks {
|
||||
|
||||
private final LinkBuilder namespaceStrategiesLinkBuilder;
|
||||
|
||||
NamespaceStrategiesLinks(ScmPathInfo pathInfo) {
|
||||
namespaceStrategiesLinkBuilder = new LinkBuilder(pathInfo, NamespaceStrategyResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return namespaceStrategiesLinkBuilder.method("get").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public RepositoryTypeLinks repositoryType() {
|
||||
return new RepositoryTypeLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.time.Clock;
|
||||
import java.time.Year;
|
||||
|
||||
@Extension
|
||||
public class CurrentYearNamespaceStrategy implements NamespaceStrategy {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
public CurrentYearNamespaceStrategy() {
|
||||
this(Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CurrentYearNamespaceStrategy(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return String.valueOf(Year.now(clock).getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.util.ValidationUtil;
|
||||
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
|
||||
@Extension
|
||||
public class CustomNamespaceStrategy implements NamespaceStrategy {
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
String namespace = repository.getNamespace();
|
||||
|
||||
doThrow()
|
||||
.violation("invalid namespace", "namespace")
|
||||
.when(!ValidationUtil.isRepositoryNameValid(namespace));
|
||||
|
||||
return namespace;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
/**
|
||||
* The DefaultNamespaceStrategy returns the predefined namespace of the given repository, if the namespace was not set
|
||||
* the username of the currently loggedin user is used.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Extension
|
||||
public class DefaultNamespaceStrategy implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
String namespace = repository.getNamespace();
|
||||
if (Strings.isNullOrEmpty(namespace)) {
|
||||
namespace = SecurityUtils.getSubject().getPrincipal().toString();
|
||||
}
|
||||
return namespace;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import sonia.scm.util.CollectionAppender;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -85,7 +86,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
private final KeyGenerator keyGenerator;
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
private final Set<Type> types;
|
||||
private NamespaceStrategy namespaceStrategy;
|
||||
private final Provider<NamespaceStrategy> namespaceStrategyProvider;
|
||||
private final ManagerDaoAdapter<Repository> managerDaoAdapter;
|
||||
|
||||
|
||||
@@ -93,11 +94,11 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
public DefaultRepositoryManager(ScmConfiguration configuration,
|
||||
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
|
||||
NamespaceStrategy namespaceStrategy) {
|
||||
Provider<NamespaceStrategy> namespaceStrategyProvider) {
|
||||
this.configuration = configuration;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.namespaceStrategy = namespaceStrategy;
|
||||
this.namespaceStrategyProvider = namespaceStrategyProvider;
|
||||
|
||||
ThreadFactory factory = new ThreadFactoryBuilder()
|
||||
.setNameFormat(THREAD_NAME).build();
|
||||
@@ -131,7 +132,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
|
||||
public Repository create(Repository repository, boolean initRepository) {
|
||||
repository.setId(keyGenerator.createKey());
|
||||
repository.setNamespace(namespaceStrategy.createNamespace(repository));
|
||||
repository.setNamespace(namespaceStrategyProvider.get().createNamespace(repository));
|
||||
|
||||
logger.info("create repository {}/{} of type {} in namespace {}", repository.getNamespace(), repository.getName(), repository.getType(), repository.getNamespace());
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -8,6 +10,8 @@ import java.util.Set;
|
||||
|
||||
public class NamespaceStrategyProvider implements Provider<NamespaceStrategy> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NamespaceStrategyProvider.class);
|
||||
|
||||
private final Set<NamespaceStrategy> strategies;
|
||||
private final ScmConfiguration scmConfiguration;
|
||||
|
||||
@@ -19,14 +23,16 @@ public class NamespaceStrategyProvider implements Provider<NamespaceStrategy> {
|
||||
|
||||
@Override
|
||||
public NamespaceStrategy get() {
|
||||
String namespaceStrategy = scmConfiguration.getDefaultNamespaceStrategy();
|
||||
String namespaceStrategy = scmConfiguration.getNamespaceStrategy();
|
||||
|
||||
for (NamespaceStrategy s : this.strategies) {
|
||||
if (s.getClass().getCanonicalName().equals(namespaceStrategy)) {
|
||||
if (s.getClass().getSimpleName().equals(namespaceStrategy)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
LOG.warn("could not find namespace strategy {}, using default strategy", namespaceStrategy);
|
||||
return new UsernameNamespaceStrategy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Set;
|
||||
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
|
||||
public class NamespaceStrategyValidator {
|
||||
|
||||
private final Set<NamespaceStrategy> strategies;
|
||||
|
||||
@Inject
|
||||
public NamespaceStrategyValidator(Set<NamespaceStrategy> strategies) {
|
||||
this.strategies = strategies;
|
||||
}
|
||||
|
||||
public void check(String name) {
|
||||
doThrow()
|
||||
.violation("unknown NamespaceStrategy " + name, "namespaceStrategy")
|
||||
.when(!isValid(name));
|
||||
}
|
||||
|
||||
private boolean isValid(String name) {
|
||||
return strategies.stream().anyMatch(ns -> ns.getClass().getSimpleName().equals(name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
public class RepositoryTypeNamespaceStrategy implements NamespaceStrategy {
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return repository.getType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
public class UsernameNamespaceStrategy implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return SecurityUtils.getSubject().getPrincipal().toString();
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@
|
||||
"errorCode": "Fehlercode",
|
||||
"transactionId": "Transaktions-ID",
|
||||
"moreInfo": "Für mehr Informationen, siehe",
|
||||
"violations": "Ungültige Werte:",
|
||||
"AGR7UzkhA1": {
|
||||
"displayName": "Nicht gefunden",
|
||||
"description": "Der gewünschte Datensatz konnte nicht gefunden werden. Möglicherweise wurde er in einer weiteren Session gelöscht."
|
||||
@@ -147,5 +148,11 @@
|
||||
"displayName": "Ungültige Eingabe",
|
||||
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
"UsernameNamespaceStrategy": "Benutzername",
|
||||
"CustomNamespaceStrategy": "Benutzerdefiniert",
|
||||
"CurrentYearNamespaceStrategy": "Aktuelles Jahr",
|
||||
"RepositoryTypeNamespaceStrategy": "Repository Typ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"errorCode": "Error Code",
|
||||
"transactionId": "Transaction ID",
|
||||
"moreInfo": "For more information, see",
|
||||
"violations": "Violations:",
|
||||
"AGR7UzkhA1": {
|
||||
"displayName": "Not found",
|
||||
"description": "The requested entity could not be found. It may have been deleted in another session."
|
||||
@@ -147,5 +148,11 @@
|
||||
"displayName": "Illegal input",
|
||||
"description": "The values could not be validated. Please correct your input and try again."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
"UsernameNamespaceStrategy": "Username",
|
||||
"CustomNamespaceStrategy": "Custom",
|
||||
"CurrentYearNamespaceStrategy": "Current year",
|
||||
"RepositoryTypeNamespaceStrategy": "Repository type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
|
||||
assertEquals("https://plug.ins" , config.getPluginUrl());
|
||||
assertEquals(40 , config.getLoginAttemptLimitTimeout());
|
||||
assertTrue(config.isEnabledXsrfProtection());
|
||||
assertEquals("username", config.getDefaultNamespaceStrategy());
|
||||
assertEquals("username", config.getNamespaceStrategy());
|
||||
}
|
||||
|
||||
private ConfigDto createDefaultDto() {
|
||||
@@ -76,7 +76,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
|
||||
configDto.setPluginUrl("https://plug.ins");
|
||||
configDto.setLoginAttemptLimitTimeout(40);
|
||||
configDto.setEnabledXsrfProtection(true);
|
||||
configDto.setDefaultNamespaceStrategy("username");
|
||||
configDto.setNamespaceStrategy("username");
|
||||
|
||||
return configDto;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.NamespaceStrategyValidator;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -22,10 +24,12 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
@SubjectAware(
|
||||
@@ -46,6 +50,9 @@ public class ConfigResourceTest {
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@Mock
|
||||
private NamespaceStrategyValidator namespaceStrategyValidator;
|
||||
|
||||
@InjectMocks
|
||||
private ConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper;
|
||||
@InjectMocks
|
||||
@@ -62,7 +69,7 @@ public class ConfigResourceTest {
|
||||
public void prepareEnvironment() {
|
||||
initMocks(this);
|
||||
|
||||
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration());
|
||||
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator);
|
||||
|
||||
dispatcher.getRegistry().addSingletonResource(configResource);
|
||||
}
|
||||
@@ -140,6 +147,21 @@ public class ConfigResourceTest {
|
||||
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readWrite")
|
||||
public void shouldValidateNamespaceStrategy() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2)
|
||||
.contentType(VndMediaType.CONFIG)
|
||||
.content("{ \"namespaceStrategy\": \"AwesomeStrategy\" }".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
verify(namespaceStrategyValidator).check("AwesomeStrategy");
|
||||
}
|
||||
|
||||
private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException {
|
||||
URL url = Resources.getResource(resourceName);
|
||||
byte[] configJson = Resources.toByteArray(url);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NamespaceStrategyResourceTest {
|
||||
|
||||
@Mock
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Test
|
||||
void shouldReturnNamespaceStrategies() {
|
||||
when(uriInfo.getAbsolutePath()).thenReturn(URI.create("/namespace-strategies"));
|
||||
|
||||
Set<NamespaceStrategy> namespaceStrategies = allStrategies();
|
||||
Provider<NamespaceStrategy> current = Providers.of(new MegaNamespaceStrategy());
|
||||
|
||||
NamespaceStrategyResource resource = new NamespaceStrategyResource(namespaceStrategies, current);
|
||||
|
||||
NamespaceStrategiesDto dto = resource.get(uriInfo);
|
||||
assertThat(dto.getCurrent()).isEqualTo(MegaNamespaceStrategy.class.getSimpleName());
|
||||
assertThat(dto.getAvailable()).contains(
|
||||
AwesomeNamespaceStrategy.class.getSimpleName(),
|
||||
SuperNamespaceStrategy.class.getSimpleName(),
|
||||
MegaNamespaceStrategy.class.getSimpleName()
|
||||
);
|
||||
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/namespace-strategies");
|
||||
}
|
||||
|
||||
private Set<NamespaceStrategy> allStrategies() {
|
||||
return strategies(new AwesomeNamespaceStrategy(), new SuperNamespaceStrategy(), new MegaNamespaceStrategy());
|
||||
}
|
||||
|
||||
private Set<NamespaceStrategy> strategies(NamespaceStrategy... strategies) {
|
||||
return new LinkedHashSet<>(Lists.newArrayList(strategies));
|
||||
}
|
||||
|
||||
private static class AwesomeNamespaceStrategy implements NamespaceStrategy {
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "awesome";
|
||||
}
|
||||
}
|
||||
|
||||
private static class SuperNamespaceStrategy implements NamespaceStrategy {
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "super";
|
||||
}
|
||||
}
|
||||
|
||||
private static class MegaNamespaceStrategy implements NamespaceStrategy {
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "mega";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ public class ResourceLinksMock {
|
||||
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
|
||||
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
|
||||
when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo));
|
||||
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
|
||||
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo));
|
||||
|
||||
return resourceLinks;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
|
||||
assertEquals("pluginurl" , dto.getPluginUrl());
|
||||
assertEquals(2 , dto.getLoginAttemptLimitTimeout());
|
||||
assertTrue(dto.isEnabledXsrfProtection());
|
||||
assertEquals("username", dto.getDefaultNamespaceStrategy());
|
||||
assertEquals("username", dto.getNamespaceStrategy());
|
||||
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
|
||||
@@ -121,7 +121,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
|
||||
config.setPluginUrl("pluginurl");
|
||||
config.setLoginAttemptLimitTimeout(2);
|
||||
config.setEnabledXsrfProtection(true);
|
||||
config.setDefaultNamespaceStrategy("username");
|
||||
config.setNamespaceStrategy("username");
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CurrentYearNamespaceStrategyTest {
|
||||
|
||||
@Mock
|
||||
private Clock clock;
|
||||
private NamespaceStrategy namespaceStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setupObjectUnderTest() {
|
||||
namespaceStrategy = new CurrentYearNamespaceStrategy(clock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn1985() {
|
||||
LocalDateTime dateTime = LocalDateTime.of(1985, 4, 9, 21, 42);
|
||||
when(clock.instant()).thenReturn(dateTime.toInstant(ZoneOffset.UTC));
|
||||
when(clock.getZone()).thenReturn(ZoneId.systemDefault());
|
||||
|
||||
String namespace = namespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold());
|
||||
assertThat(namespace).isEqualTo("1985");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.ScmConstraintViolationException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CustomNamespaceStrategyTest {
|
||||
|
||||
private final NamespaceStrategy namespaceStrategy = new CustomNamespaceStrategy();
|
||||
|
||||
@Test
|
||||
void shouldReturnNamespaceFromRepository() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
assertThat(namespaceStrategy.createNamespace(heartOfGold)).isEqualTo(RepositoryTestData.NAMESPACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAnValidationExceptionForAnInvalidNamespace() {
|
||||
Repository repository = new Repository();
|
||||
repository.setNamespace("..");
|
||||
repository.setName(".");
|
||||
|
||||
assertThrows(ScmConstraintViolationException.class, () -> namespaceStrategy.createNamespace(repository));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
|
||||
public class DefaultNamespaceStrategyTest {
|
||||
|
||||
@Rule
|
||||
public ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private DefaultNamespaceStrategy namespaceStrategy = new DefaultNamespaceStrategy();
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void testNamespaceStrategyWithoutPreset() {
|
||||
assertEquals("trillian", namespaceStrategy.createNamespace(new Repository()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void testNamespaceStrategyWithPreset() {
|
||||
Repository repository = new Repository();
|
||||
repository.setNamespace("awesome");
|
||||
assertEquals("awesome", namespaceStrategy.createNamespace(repository));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import com.google.common.base.Stopwatch;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
@@ -117,8 +118,8 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
contextProvider,
|
||||
keyGenerator,
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
namespaceStrategy
|
||||
handlerSet,
|
||||
Providers.of(namespaceStrategy)
|
||||
);
|
||||
|
||||
setUpTestRepositories();
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.Before;
|
||||
@@ -436,7 +437,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace);
|
||||
|
||||
return new DefaultRepositoryManager(configuration, contextProvider,
|
||||
keyGenerator, repositoryDAO, handlerSet, namespaceStrategy);
|
||||
keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy));
|
||||
}
|
||||
|
||||
private void createRepository(RepositoryManager m, Repository repository) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class NamespaceStrategyProviderTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnConfiguredStrategy() {
|
||||
Set<NamespaceStrategy> strategies = allStrategiesAsSet();
|
||||
|
||||
ScmConfiguration configuration = new ScmConfiguration();
|
||||
configuration.setNamespaceStrategy("Arthur");
|
||||
|
||||
NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration);
|
||||
NamespaceStrategy strategy = provider.get();
|
||||
|
||||
assertThat(strategy).isInstanceOf(Arthur.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnUsernameStrategyForUnknown() {
|
||||
Set<NamespaceStrategy> strategies = Collections.emptySet();
|
||||
|
||||
ScmConfiguration configuration = new ScmConfiguration();
|
||||
configuration.setNamespaceStrategy("Arthur");
|
||||
|
||||
NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration);
|
||||
NamespaceStrategy strategy = provider.get();
|
||||
|
||||
assertThat(strategy).isInstanceOf(UsernameNamespaceStrategy.class);
|
||||
}
|
||||
|
||||
private LinkedHashSet<NamespaceStrategy> allStrategiesAsSet() {
|
||||
return new LinkedHashSet<>(Arrays.asList(new Trillian(), new Zaphod(), new Arthur()));
|
||||
}
|
||||
|
||||
private static class Trillian implements NamespaceStrategy{
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "trillian";
|
||||
}
|
||||
}
|
||||
|
||||
private static class Zaphod implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "zaphod";
|
||||
}
|
||||
}
|
||||
|
||||
private static class Arthur implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return "arthur";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.ScmConstraintViolationException;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class NamespaceStrategyValidatorTest {
|
||||
|
||||
@Test
|
||||
void shouldThrowConstraintValidationException() {
|
||||
NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Collections.emptySet());
|
||||
assertThrows(ScmConstraintViolationException.class, () -> validator.check("AwesomeStrategy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDoNotThrowAnException() {
|
||||
NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Sets.newHashSet(new AwesomeStrategy()));
|
||||
validator.check("AwesomeStrategy");
|
||||
}
|
||||
|
||||
public static class AwesomeStrategy implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
public String createNamespace(Repository repository) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RepositoryTypeNamespaceStrategyTest {
|
||||
|
||||
private final RepositoryTypeNamespaceStrategy namespaceStrategy = new RepositoryTypeNamespaceStrategy();
|
||||
|
||||
@Test
|
||||
void shouldReturnTypeOfRepository() {
|
||||
Repository git = RepositoryTestData.create42Puzzle("git");
|
||||
assertThat(namespaceStrategy.createNamespace(git)).isEqualTo("git");
|
||||
|
||||
Repository hg = RepositoryTestData.create42Puzzle("hg");
|
||||
assertThat(namespaceStrategy.createNamespace(hg)).isEqualTo("hg");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UsernameNamespaceStrategyTest {
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
private final NamespaceStrategy usernameNamespaceStrategy = new UsernameNamespaceStrategy();
|
||||
|
||||
@BeforeEach
|
||||
void setupSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearThreadContext() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPrimaryPrincipal() {
|
||||
when(subject.getPrincipal()).thenReturn("trillian");
|
||||
|
||||
String namespace = usernameNamespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold());
|
||||
assertThat(namespace).isEqualTo("trillian");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user