mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 08:55:44 +01:00
Notifications for health checks (#1664)
Add list of emergency contacts to global configuration. This user will receive e-mails and notification if some serious system error occurs like repository health check failed.
This commit is contained in:
@@ -41,6 +41,9 @@ Ist der Benutzer Konverter aktiviert, werden alle internen Benutzer beim Einlogg
|
||||
#### Fallback E-Mail Domain Name
|
||||
Dieser Domain Name wird genutzt, wenn für einen User eine E-Mail-Adresse benötigt wird, für den keine hinterlegt ist. Diese Domain wird nicht zum Versenden von E-Mails genutzt und auch keine anderweitige Verbindung aufgebaut.
|
||||
|
||||
#### Notfallkontakte
|
||||
Die folgenden Benutzer werden über administrative Vorfälle informiert (z. B. fehlgeschlagene Integritätsprüfungen).
|
||||
|
||||
#### Anmeldeversuche
|
||||
Es lässt sich konfigurieren wie häufig sich ein Benutzer falsch anmelden darf, bevor dessen Benutzerkonto gesperrt wird. Der Zähler für fehlerhafte Anmeldeversuche wird nach einem erfolgreichen Login zurückgesetzt. Man kann dieses Feature abschalten, indem man "-1" in die Konfiguration einträgt.
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ Internal users will automatically be converted to external on their first login
|
||||
#### Fallback Mail Domain Name
|
||||
This domain name will be used to create email addresses for users without one when needed. It will not be used to send mails nor will be accessed otherwise.
|
||||
|
||||
#### Emergency Contacts
|
||||
The following users will be notified of administrative incidents (e.g. failed health checks).
|
||||
|
||||
#### Login Attempt Limit
|
||||
It can be configured how many failed login attempts a user can have before the account gets disabled. The counter for failed login attempts is reset after a successful login. This feature can be deactivated by setting the value "-1".
|
||||
|
||||
|
||||
2
gradle/changelog/health_check_notifications.yaml
Normal file
2
gradle/changelog/health_check_notifications.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Notifications for health checks ([#1664](https://github.com/scm-manager/scm-manager/pull/1664))
|
||||
@@ -214,6 +214,14 @@ public class ScmConfiguration implements Configuration {
|
||||
@XmlElement(name = "mail-domain-name")
|
||||
private String mailDomainName = DEFAULT_MAIL_DOMAIN_NAME;
|
||||
|
||||
/**
|
||||
* List of users that will be notified of administrative incidents.
|
||||
*
|
||||
* @since 2.19.0
|
||||
*/
|
||||
@XmlElement(name = "emergency-contacts")
|
||||
private Set<String> emergencyContacts;
|
||||
|
||||
/**
|
||||
* Fires the {@link ScmConfigurationChangedEvent}.
|
||||
*/
|
||||
@@ -253,6 +261,7 @@ public class ScmConfiguration implements Configuration {
|
||||
this.loginInfoUrl = other.loginInfoUrl;
|
||||
this.releaseFeedUrl = other.releaseFeedUrl;
|
||||
this.mailDomainName = other.mailDomainName;
|
||||
this.emergencyContacts = other.emergencyContacts;
|
||||
this.enabledUserConverter = other.enabledUserConverter;
|
||||
this.enabledApiKeys = other.enabledApiKeys;
|
||||
}
|
||||
@@ -456,6 +465,14 @@ public class ScmConfiguration implements Configuration {
|
||||
return skipFailedAuthenticators;
|
||||
}
|
||||
|
||||
public Set<String> getEmergencyContacts() {
|
||||
if (emergencyContacts == null) {
|
||||
emergencyContacts = Sets.newHashSet();
|
||||
}
|
||||
|
||||
return emergencyContacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the anonymous access at protocol level.
|
||||
*
|
||||
@@ -621,6 +638,10 @@ public class ScmConfiguration implements Configuration {
|
||||
this.loginInfoUrl = loginInfoUrl;
|
||||
}
|
||||
|
||||
public void setEmergencyContacts(Set<String> emergencyContacts) {
|
||||
this.emergencyContacts = emergencyContacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Only for permission checks, don't serialize to XML
|
||||
@XmlTransient
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("Test config hooks", () => {
|
||||
loginInfoUrl: "",
|
||||
mailDomainName: "",
|
||||
namespaceStrategy: "",
|
||||
emergencyContacts: [],
|
||||
pluginUrl: "",
|
||||
proxyExcludes: [],
|
||||
proxyPassword: null,
|
||||
|
||||
@@ -32,6 +32,7 @@ export * from "./base";
|
||||
export * from "./login";
|
||||
export * from "./groups";
|
||||
export * from "./users";
|
||||
export * from "./userSuggestions";
|
||||
export * from "./repositories";
|
||||
export * from "./namespaces";
|
||||
export * from "./branches";
|
||||
|
||||
52
scm-ui/ui-api/src/userSuggestions.ts
Normal file
52
scm-ui/ui-api/src/userSuggestions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import {DisplayedUser, Link, SelectValue} from "@scm-manager/ui-types";
|
||||
import { useIndexLinks } from "./base";
|
||||
import { apiClient } from "./apiclient";
|
||||
|
||||
export const useUserSuggestions = () => {
|
||||
const indexLinks = useIndexLinks();
|
||||
const autocompleteLink = (indexLinks.autocomplete as Link[]).find(i => i.name === "users");
|
||||
if (!autocompleteLink) {
|
||||
return [];
|
||||
}
|
||||
const url = autocompleteLink.href + "?q=";
|
||||
return (inputValue: string): never[] | Promise<SelectValue[]> => {
|
||||
// Prevent violate input condition of api call because parameter length is too short
|
||||
if (inputValue.length < 2) {
|
||||
return [];
|
||||
}
|
||||
return apiClient
|
||||
.get(url + inputValue)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
return json.map((element: DisplayedUser) => {
|
||||
return {
|
||||
value: element,
|
||||
label: `${element.displayName} (${element.id})`
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -32,7 +32,7 @@ type Props = {
|
||||
addEntry: (p: SelectValue) => void;
|
||||
disabled?: boolean;
|
||||
buttonLabel: string;
|
||||
fieldLabel: string;
|
||||
fieldLabel?: string;
|
||||
helpText?: string;
|
||||
loadSuggestions: (p: string) => Promise<SelectValue[]>;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -50,5 +50,6 @@ export type Config = HalRepresentation & {
|
||||
loginInfoUrl: string;
|
||||
releaseFeedUrl: string;
|
||||
mailDomainName: string;
|
||||
emergencyContacts: string[];
|
||||
enabledApiKeys: boolean;
|
||||
};
|
||||
|
||||
@@ -61,7 +61,13 @@
|
||||
"enabled-user-converter": "Benutzer Konverter aktivieren",
|
||||
"enabled-api-keys": "API Schlüssel aktivieren",
|
||||
"namespace-strategy": "Namespace Strategie",
|
||||
"login-info-url": "Login Info URL"
|
||||
"login-info-url": "Login Info URL",
|
||||
"emergencyContacts": {
|
||||
"label": "Notfallkontakte",
|
||||
"helpText": "Liste der Benutzer, die über administrative Vorfälle informiert werden.",
|
||||
"addButton": "Kontakt hinzufügen",
|
||||
"autocompletePlaceholder": "Nutzer zum Benachrichtigen hinzufügen"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"date-format-invalid": "Das Datumsformat ist ungültig",
|
||||
|
||||
@@ -61,7 +61,13 @@
|
||||
"enabled-user-converter": "Enabled User Converter",
|
||||
"enabled-api-keys": "Enabled API Keys",
|
||||
"namespace-strategy": "Namespace Strategy",
|
||||
"login-info-url": "Login Info URL"
|
||||
"login-info-url": "Login Info URL",
|
||||
"emergencyContacts": {
|
||||
"label": "Emergency Contacts",
|
||||
"helpText": "List of users notified of administrative incidents.",
|
||||
"addButton": "Add Contact",
|
||||
"autocompletePlaceholder": "Add User to Notify"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"date-format-invalid": "The date format is not valid",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, useState, useEffect, FormEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Config, NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import { Level, Notification, SubmitButton } from "@scm-manager/ui-components";
|
||||
import ProxySettings from "./ProxySettings";
|
||||
@@ -30,7 +30,7 @@ import GeneralSettings from "./GeneralSettings";
|
||||
import BaseUrlSettings from "./BaseUrlSettings";
|
||||
import LoginAttempt from "./LoginAttempt";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
submitForm: (p: Config) => void;
|
||||
config?: Config;
|
||||
loading?: boolean;
|
||||
@@ -39,22 +39,16 @@ type Props = WithTranslation & {
|
||||
namespaceStrategies?: NamespaceStrategies;
|
||||
};
|
||||
|
||||
type State = {
|
||||
config: Config;
|
||||
showNotification: boolean;
|
||||
error: {
|
||||
loginAttemptLimitTimeout: boolean;
|
||||
loginAttemptLimit: boolean;
|
||||
};
|
||||
changed: boolean;
|
||||
};
|
||||
|
||||
class ConfigForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
config: {
|
||||
const ConfigForm: FC<Props> = ({
|
||||
submitForm,
|
||||
config,
|
||||
loading,
|
||||
configReadPermission,
|
||||
configUpdatePermission,
|
||||
namespaceStrategies
|
||||
}) => {
|
||||
const [t] = useTranslation("config");
|
||||
const [innerConfig, setInnerConfig] = useState<Config>({
|
||||
proxyPassword: null,
|
||||
proxyPort: 0,
|
||||
proxyServer: "",
|
||||
@@ -63,9 +57,9 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
realmDescription: "",
|
||||
disableGroupingGrid: false,
|
||||
dateFormat: "",
|
||||
anonymousAccessEnabled: false,
|
||||
anonymousMode: "OFF",
|
||||
baseUrl: "",
|
||||
mailDomainName: "",
|
||||
forceBaseUrl: false,
|
||||
loginAttemptLimit: 0,
|
||||
proxyExcludes: [],
|
||||
@@ -76,46 +70,50 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
enabledUserConverter: false,
|
||||
namespaceStrategy: "",
|
||||
loginInfoUrl: "",
|
||||
releaseFeedUrl: "",
|
||||
mailDomainName: "",
|
||||
emergencyContacts: [],
|
||||
enabledApiKeys: true,
|
||||
_links: {}
|
||||
},
|
||||
showNotification: false,
|
||||
error: {
|
||||
});
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [error, setError] = useState<{
|
||||
loginAttemptLimitTimeout: boolean;
|
||||
loginAttemptLimit: boolean;
|
||||
}>({
|
||||
loginAttemptLimitTimeout: false,
|
||||
loginAttemptLimit: false
|
||||
},
|
||||
changed: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { config, configUpdatePermission } = this.props;
|
||||
if (config) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
config: {
|
||||
...config
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setInnerConfig(config);
|
||||
}
|
||||
if (!configUpdatePermission) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
showNotification: true
|
||||
});
|
||||
}
|
||||
setShowNotification(true);
|
||||
}
|
||||
}, [config, configUpdatePermission]);
|
||||
|
||||
submit = (event: Event) => {
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
changed: false
|
||||
});
|
||||
this.props.submitForm(this.state.config);
|
||||
setChanged(false);
|
||||
submitForm(innerConfig);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, t, namespaceStrategies, configReadPermission, configUpdatePermission } = this.props;
|
||||
const config = this.state.config;
|
||||
const onChange = (isValid: boolean, changedValue: any, name: string) => {
|
||||
setInnerConfig({ ...innerConfig, [name]: changedValue });
|
||||
setError({ ...error, [name]: !isValid });
|
||||
setChanged(true);
|
||||
};
|
||||
|
||||
const hasError = () => {
|
||||
return error.loginAttemptLimit || error.loginAttemptLimitTimeout;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setShowNotification(false);
|
||||
};
|
||||
|
||||
let noPermissionNotification = null;
|
||||
|
||||
@@ -123,60 +121,61 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
return <Notification type={"danger"} children={t("config.form.no-read-permission-notification")} />;
|
||||
}
|
||||
|
||||
if (this.state.showNotification) {
|
||||
if (showNotification) {
|
||||
noPermissionNotification = (
|
||||
<Notification
|
||||
type={"info"}
|
||||
children={t("config.form.no-write-permission-notification")}
|
||||
onClose={() => this.onClose()}
|
||||
onClose={() => onClose()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
<form onSubmit={submit}>
|
||||
{noPermissionNotification}
|
||||
<GeneralSettings
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
loginInfoUrl={config.loginInfoUrl}
|
||||
realmDescription={config.realmDescription}
|
||||
disableGroupingGrid={config.disableGroupingGrid}
|
||||
dateFormat={config.dateFormat}
|
||||
anonymousMode={config.anonymousMode}
|
||||
skipFailedAuthenticators={config.skipFailedAuthenticators}
|
||||
pluginUrl={config.pluginUrl}
|
||||
releaseFeedUrl={config.releaseFeedUrl}
|
||||
mailDomainName={config.mailDomainName}
|
||||
enabledXsrfProtection={config.enabledXsrfProtection}
|
||||
enabledUserConverter={config.enabledUserConverter}
|
||||
enabledApiKeys={config.enabledApiKeys}
|
||||
namespaceStrategy={config.namespaceStrategy}
|
||||
onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)}
|
||||
loginInfoUrl={innerConfig.loginInfoUrl}
|
||||
realmDescription={innerConfig.realmDescription}
|
||||
disableGroupingGrid={innerConfig.disableGroupingGrid}
|
||||
dateFormat={innerConfig.dateFormat}
|
||||
anonymousMode={innerConfig.anonymousMode}
|
||||
skipFailedAuthenticators={innerConfig.skipFailedAuthenticators}
|
||||
pluginUrl={innerConfig.pluginUrl}
|
||||
releaseFeedUrl={innerConfig.releaseFeedUrl}
|
||||
mailDomainName={innerConfig.mailDomainName}
|
||||
enabledXsrfProtection={innerConfig.enabledXsrfProtection}
|
||||
enabledUserConverter={innerConfig.enabledUserConverter}
|
||||
enabledApiKeys={innerConfig.enabledApiKeys}
|
||||
emergencyContacts={innerConfig.emergencyContacts}
|
||||
namespaceStrategy={innerConfig.namespaceStrategy}
|
||||
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<LoginAttempt
|
||||
loginAttemptLimit={config.loginAttemptLimit}
|
||||
loginAttemptLimitTimeout={config.loginAttemptLimitTimeout}
|
||||
onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)}
|
||||
loginAttemptLimit={innerConfig.loginAttemptLimit}
|
||||
loginAttemptLimitTimeout={innerConfig.loginAttemptLimitTimeout}
|
||||
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<BaseUrlSettings
|
||||
baseUrl={config.baseUrl}
|
||||
forceBaseUrl={config.forceBaseUrl}
|
||||
onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)}
|
||||
baseUrl={innerConfig.baseUrl}
|
||||
forceBaseUrl={innerConfig.forceBaseUrl}
|
||||
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<ProxySettings
|
||||
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
|
||||
proxyPort={config.proxyPort}
|
||||
proxyServer={config.proxyServer ? config.proxyServer : ""}
|
||||
proxyUser={config.proxyUser ? config.proxyUser : ""}
|
||||
enableProxy={config.enableProxy}
|
||||
proxyExcludes={config.proxyExcludes}
|
||||
onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)}
|
||||
proxyPassword={innerConfig.proxyPassword ? innerConfig.proxyPassword : ""}
|
||||
proxyPort={innerConfig.proxyPort ? innerConfig.proxyPort : 0}
|
||||
proxyServer={innerConfig.proxyServer ? innerConfig.proxyServer : ""}
|
||||
proxyUser={innerConfig.proxyUser ? innerConfig.proxyUser : ""}
|
||||
enableProxy={innerConfig.enableProxy}
|
||||
proxyExcludes={innerConfig.proxyExcludes}
|
||||
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
@@ -185,39 +184,12 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
label={t("config.form.submit")}
|
||||
disabled={!configUpdatePermission || this.hasError() || !this.state.changed}
|
||||
disabled={!configUpdatePermission || hasError() || !changed}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
onChange = (isValid: boolean, changedValue: any, name: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
config: {
|
||||
...this.state.config,
|
||||
[name]: changedValue
|
||||
},
|
||||
error: {
|
||||
...this.state.error,
|
||||
[name]: !isValid
|
||||
},
|
||||
changed: true
|
||||
});
|
||||
};
|
||||
|
||||
hasError = () => {
|
||||
return this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout;
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
showNotification: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("config")(ConfigForm);
|
||||
export default ConfigForm;
|
||||
|
||||
@@ -21,13 +21,20 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Checkbox, InputField, Select } from "@scm-manager/ui-components";
|
||||
import { NamespaceStrategies, AnonymousMode } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUserSuggestions } from "@scm-manager/ui-api";
|
||||
import { NamespaceStrategies, AnonymousMode, SelectValue } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Checkbox,
|
||||
InputField,
|
||||
MemberNameTagGroup,
|
||||
AutocompleteAddEntryToTableField,
|
||||
Select
|
||||
} from "@scm-manager/ui-components";
|
||||
import NamespaceStrategySelect from "./NamespaceStrategySelect";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
realmDescription: string;
|
||||
loginInfoUrl: string;
|
||||
disableGroupingGrid: boolean;
|
||||
@@ -40,29 +47,76 @@ type Props = WithTranslation & {
|
||||
enabledXsrfProtection: boolean;
|
||||
enabledUserConverter: boolean;
|
||||
enabledApiKeys: boolean;
|
||||
emergencyContacts: string[];
|
||||
namespaceStrategy: string;
|
||||
namespaceStrategies?: NamespaceStrategies;
|
||||
onChange: (p1: boolean, p2: any, p3: string) => void;
|
||||
hasUpdatePermission: boolean;
|
||||
};
|
||||
|
||||
class GeneralSettings extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
const GeneralSettings: FC<Props> = ({
|
||||
realmDescription,
|
||||
loginInfoUrl,
|
||||
anonymousMode,
|
||||
pluginUrl,
|
||||
releaseFeedUrl,
|
||||
mailDomainName,
|
||||
enabledXsrfProtection,
|
||||
enabledUserConverter,
|
||||
enabledApiKeys,
|
||||
anonymousMode,
|
||||
emergencyContacts,
|
||||
namespaceStrategy,
|
||||
hasUpdatePermission,
|
||||
namespaceStrategies
|
||||
} = this.props;
|
||||
namespaceStrategies,
|
||||
onChange,
|
||||
hasUpdatePermission
|
||||
}) => {
|
||||
const { t } = useTranslation("config");
|
||||
const userSuggestions = useUserSuggestions();
|
||||
|
||||
const handleLoginInfoUrlChange = (value: string) => {
|
||||
onChange(true, value, "loginInfoUrl");
|
||||
};
|
||||
const handleRealmDescriptionChange = (value: string) => {
|
||||
onChange(true, value, "realmDescription");
|
||||
};
|
||||
const handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledXsrfProtection");
|
||||
};
|
||||
const handleEnabledUserConverterChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledUserConverter");
|
||||
};
|
||||
const handleAnonymousMode = (value: string) => {
|
||||
onChange(true, value, "anonymousMode");
|
||||
};
|
||||
const handleNamespaceStrategyChange = (value: string) => {
|
||||
onChange(true, value, "namespaceStrategy");
|
||||
};
|
||||
const handlePluginCenterUrlChange = (value: string) => {
|
||||
onChange(true, value, "pluginUrl");
|
||||
};
|
||||
const handleReleaseFeedUrlChange = (value: string) => {
|
||||
onChange(true, value, "releaseFeedUrl");
|
||||
};
|
||||
const handleMailDomainNameChange = (value: string) => {
|
||||
onChange(true, value, "mailDomainName");
|
||||
};
|
||||
const handleEnabledApiKeysChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledApiKeys");
|
||||
};
|
||||
const handleEmergencyContactsChange = (p: string[]) => {
|
||||
onChange(true, p, "emergencyContacts");
|
||||
};
|
||||
|
||||
const isMember = (name: string) => {
|
||||
return emergencyContacts.includes(name);
|
||||
};
|
||||
|
||||
const addEmergencyContact = (value: SelectValue) => {
|
||||
if (isMember(value.value.id)) {
|
||||
return;
|
||||
}
|
||||
handleEmergencyContactsChange([...emergencyContacts, value.value.id]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -70,7 +124,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.realm-description")}
|
||||
onChange={this.handleRealmDescriptionChange}
|
||||
onChange={handleRealmDescriptionChange}
|
||||
value={realmDescription}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.realmDescriptionHelpText")}
|
||||
@@ -79,7 +133,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<NamespaceStrategySelect
|
||||
label={t("general-settings.namespace-strategy")}
|
||||
onChange={this.handleNamespaceStrategyChange}
|
||||
onChange={handleNamespaceStrategyChange}
|
||||
value={namespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
@@ -91,7 +145,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.login-info-url")}
|
||||
onChange={this.handleLoginInfoUrlChange}
|
||||
onChange={handleLoginInfoUrlChange}
|
||||
value={loginInfoUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.loginInfoUrlHelpText")}
|
||||
@@ -100,7 +154,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-xsrf-protection")}
|
||||
onChange={this.handleEnabledXsrfProtectionChange}
|
||||
onChange={handleEnabledXsrfProtectionChange}
|
||||
checked={enabledXsrfProtection}
|
||||
title={t("general-settings.enabled-xsrf-protection")}
|
||||
disabled={!hasUpdatePermission}
|
||||
@@ -112,7 +166,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.plugin-url")}
|
||||
onChange={this.handlePluginCenterUrlChange}
|
||||
onChange={handlePluginCenterUrlChange}
|
||||
value={pluginUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.pluginUrlHelpText")}
|
||||
@@ -121,7 +175,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<Select
|
||||
label={t("general-settings.anonymousMode.title")}
|
||||
onChange={this.handleAnonymousMode}
|
||||
onChange={handleAnonymousMode}
|
||||
value={anonymousMode}
|
||||
disabled={!hasUpdatePermission}
|
||||
options={[
|
||||
@@ -138,7 +192,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.release-feed-url")}
|
||||
onChange={this.handleReleaseFeedUrlChange}
|
||||
onChange={handleReleaseFeedUrlChange}
|
||||
value={releaseFeedUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.releaseFeedUrlHelpText")}
|
||||
@@ -147,7 +201,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-user-converter")}
|
||||
onChange={this.handleEnabledUserConverterChange}
|
||||
onChange={handleEnabledUserConverterChange}
|
||||
checked={enabledUserConverter}
|
||||
title={t("general-settings.enabled-user-converter")}
|
||||
disabled={!hasUpdatePermission}
|
||||
@@ -159,7 +213,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("general-settings.mail-domain-name")}
|
||||
onChange={this.handleMailDomainNameChange}
|
||||
onChange={handleMailDomainNameChange}
|
||||
value={mailDomainName}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.mailDomainNameHelpText")}
|
||||
@@ -168,7 +222,7 @@ class GeneralSettings extends React.Component<Props> {
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-api-keys")}
|
||||
onChange={this.handleEnabledApiKeysChange}
|
||||
onChange={handleEnabledApiKeysChange}
|
||||
checked={enabledApiKeys}
|
||||
title={t("general-settings.enabled-api-keys")}
|
||||
disabled={!hasUpdatePermission}
|
||||
@@ -176,40 +230,24 @@ class GeneralSettings extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-full">
|
||||
<MemberNameTagGroup
|
||||
members={emergencyContacts}
|
||||
memberListChanged={handleEmergencyContactsChange}
|
||||
label={t("general-settings.emergencyContacts.label")}
|
||||
helpText={t("general-settings.emergencyContacts.helpText")}
|
||||
/>
|
||||
<AutocompleteAddEntryToTableField
|
||||
addEntry={addEmergencyContact}
|
||||
buttonLabel={t("general-settings.emergencyContacts.addButton")}
|
||||
loadSuggestions={userSuggestions}
|
||||
placeholder={t("general-settings.emergencyContacts.autocompletePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleLoginInfoUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "loginInfoUrl");
|
||||
};
|
||||
handleRealmDescriptionChange = (value: string) => {
|
||||
this.props.onChange(true, value, "realmDescription");
|
||||
};
|
||||
handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enabledXsrfProtection");
|
||||
};
|
||||
handleEnabledUserConverterChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enabledUserConverter");
|
||||
};
|
||||
handleAnonymousMode = (value: string) => {
|
||||
this.props.onChange(true, value, "anonymousMode");
|
||||
};
|
||||
handleNamespaceStrategyChange = (value: string) => {
|
||||
this.props.onChange(true, value, "namespaceStrategy");
|
||||
};
|
||||
handlePluginCenterUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "pluginUrl");
|
||||
};
|
||||
handleReleaseFeedUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "releaseFeedUrl");
|
||||
};
|
||||
handleMailDomainNameChange = (value: string) => {
|
||||
this.props.onChange(true, value, "mailDomainName");
|
||||
};
|
||||
handleEnabledApiKeysChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enabledApiKeys");
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("config")(GeneralSettings);
|
||||
export default GeneralSettings;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { NamespaceStrategies } from "@scm-manager/ui-types";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
namespaceStrategies: NamespaceStrategies;
|
||||
namespaceStrategies?: NamespaceStrategies;
|
||||
label: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -22,46 +22,25 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DisplayedUser, Link } from "@scm-manager/ui-types";
|
||||
import { apiClient, Page } from "@scm-manager/ui-components";
|
||||
import GroupForm from "../components/GroupForm";
|
||||
import { useCreateGroup, useIndexLinks } from "@scm-manager/ui-api";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCreateGroup, useUserSuggestions } from "@scm-manager/ui-api";
|
||||
import { Page } from "@scm-manager/ui-components";
|
||||
import GroupForm from "../components/GroupForm";
|
||||
|
||||
const CreateGroup: FC = () => {
|
||||
const [t] = useTranslation("groups");
|
||||
const { isLoading, create, error, group } = useCreateGroup();
|
||||
const indexLinks = useIndexLinks();
|
||||
const userSuggestions = useUserSuggestions();
|
||||
|
||||
if (group) {
|
||||
return <Redirect to={`/group/${group.name}`} />;
|
||||
}
|
||||
|
||||
// TODO: Replace with react-query hook
|
||||
const loadUserAutocompletion = (inputValue: string) => {
|
||||
const autocompleteLink = (indexLinks.autocomplete as Link[]).find(i => i.name === "users");
|
||||
if (!autocompleteLink) {
|
||||
return [];
|
||||
}
|
||||
const url = autocompleteLink.href + "?q=";
|
||||
return apiClient
|
||||
.get(url + inputValue)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
return json.map((element: DisplayedUser) => {
|
||||
return {
|
||||
value: element,
|
||||
label: `${element.displayName} (${element.id})`
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title={t("add-group.title")} subtitle={t("add-group.subtitle")} error={error || undefined}>
|
||||
<div>
|
||||
<GroupForm submitForm={create} loading={isLoading} loadUserSuggestions={loadUserAutocompletion} />
|
||||
<GroupForm submitForm={create} loading={isLoading} loadUserSuggestions={userSuggestions} />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -22,49 +22,29 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import GroupForm from "../components/GroupForm";
|
||||
import { DisplayedUser, Group, Link } from "@scm-manager/ui-types";
|
||||
import { apiClient, ErrorNotification } from "@scm-manager/ui-components";
|
||||
import DeleteGroup from "./DeleteGroup";
|
||||
import { useIndexLinks, useUpdateGroup } from "@scm-manager/ui-api";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { Group } from "@scm-manager/ui-types";
|
||||
import { useUpdateGroup, useUserSuggestions } from "@scm-manager/ui-api";
|
||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||
import GroupForm from "../components/GroupForm";
|
||||
import DeleteGroup from "./DeleteGroup";
|
||||
|
||||
type Props = {
|
||||
group: Group;
|
||||
};
|
||||
|
||||
const EditGroup: FC<Props> = ({ group }) => {
|
||||
const indexLinks = useIndexLinks();
|
||||
const { error, isLoading, update, isUpdated } = useUpdateGroup();
|
||||
const autocompleteLink = (indexLinks.autocomplete as Link[]).find(i => i.name === "users");
|
||||
const userSuggestions = useUserSuggestions();
|
||||
|
||||
if (isUpdated) {
|
||||
return <Redirect to={`/group/${group.name}`} />;
|
||||
}
|
||||
|
||||
// TODO: Replace with react-query hook
|
||||
const loadUserAutocompletion = (inputValue: string) => {
|
||||
if (!autocompleteLink) {
|
||||
return [];
|
||||
}
|
||||
const url = autocompleteLink.href + "?q=";
|
||||
return apiClient
|
||||
.get(url + inputValue)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
return json.map((element: DisplayedUser) => {
|
||||
return {
|
||||
value: element,
|
||||
label: `${element.displayName} (${element.id})`
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorNotification error={error || undefined} />
|
||||
<GroupForm group={group} submitForm={update} loading={isLoading} loadUserSuggestions={loadUserAutocompletion} />
|
||||
<GroupForm group={group} submitForm={update} loading={isLoading} loadUserSuggestions={userSuggestions} />
|
||||
<DeleteGroup group={group} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -62,6 +62,7 @@ public class ConfigDto extends HalRepresentation implements UpdateConfigDto {
|
||||
private String loginInfoUrl;
|
||||
private String releaseFeedUrl;
|
||||
private String mailDomainName;
|
||||
private Set<String> emergencyContacts;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
|
||||
@@ -74,4 +74,6 @@ interface UpdateConfigDto {
|
||||
String getReleaseFeedUrl();
|
||||
|
||||
String getMailDomainName();
|
||||
|
||||
Set<String> getEmergencyContacts();
|
||||
}
|
||||
|
||||
@@ -25,9 +25,14 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.notifications.Notification;
|
||||
import sonia.scm.notifications.NotificationSender;
|
||||
import sonia.scm.notifications.Type;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -59,15 +64,22 @@ final class HealthChecker {
|
||||
|
||||
private final ExecutorService healthCheckExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final ScmConfiguration scmConfiguration;
|
||||
private final NotificationSender notificationSender;
|
||||
|
||||
@Inject
|
||||
HealthChecker(Set<HealthCheck> checks,
|
||||
RepositoryManager repositoryManager,
|
||||
RepositoryServiceFactory repositoryServiceFactory,
|
||||
RepositoryPostProcessor repositoryPostProcessor) {
|
||||
RepositoryPostProcessor repositoryPostProcessor,
|
||||
ScmConfiguration scmConfiguration,
|
||||
NotificationSender notificationSender) {
|
||||
this.checks = checks;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.repositoryServiceFactory = repositoryServiceFactory;
|
||||
this.repositoryPostProcessor = repositoryPostProcessor;
|
||||
this.scmConfiguration = scmConfiguration;
|
||||
this.notificationSender = notificationSender;
|
||||
}
|
||||
|
||||
void lightCheck(String id) {
|
||||
@@ -160,6 +172,7 @@ final class HealthChecker {
|
||||
HealthCheckResult fullCheckResult = gatherFullChecks(repository);
|
||||
HealthCheckResult result = lightCheckResult.merge(fullCheckResult);
|
||||
|
||||
notifyCurrentUser(repository, result);
|
||||
storeResult(repository, result);
|
||||
})
|
||||
);
|
||||
@@ -204,10 +217,32 @@ final class HealthChecker {
|
||||
logger.trace("store health check results for repository {}",
|
||||
repository);
|
||||
repositoryPostProcessor.setCheckResults(repository, result.getFailures());
|
||||
|
||||
notifyEmergencyContacts(repository);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkRunning(String repositoryId) {
|
||||
return checksRunning.contains(repositoryId);
|
||||
}
|
||||
|
||||
private void notifyCurrentUser(Repository repository, HealthCheckResult result) {
|
||||
if (!(repository.isHealthy() && result.isHealthy())) {
|
||||
String currentUser = SecurityUtils.getSubject().getPrincipal().toString();
|
||||
if (!scmConfiguration.getEmergencyContacts().contains(currentUser)) {
|
||||
notificationSender.send(getHealthCheckFailedNotification(repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyEmergencyContacts(Repository repository) {
|
||||
Set<String> emergencyContacts = scmConfiguration.getEmergencyContacts();
|
||||
for (String user : emergencyContacts) {
|
||||
notificationSender.send(getHealthCheckFailedNotification(repository), user);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification getHealthCheckFailedNotification(Repository repository) {
|
||||
return new Notification(Type.ERROR, "/repo/" + repository.getNamespaceAndName() + "/settings/general", "healthCheckFailed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +427,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"exportFinished": "Der Repository Export wurde abgeschlossen.",
|
||||
"exportFailed": "Der Repository Export ist fehlgeschlagen. Versuchen Sie es erneut oder wenden Sie sich an einen Administrator."
|
||||
"exportFailed": "Der Repository Export ist fehlgeschlagen. Versuchen Sie es erneut oder wenden Sie sich an einen Administrator.",
|
||||
"healthCheckFailed": "Der Repository Health Check ist fehlgeschlagen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +371,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"exportFinished": "The repository export has been finished.",
|
||||
"exportFailed": "The repository export has failed. Try it again or contact your administrator."
|
||||
"exportFailed": "The repository export has failed. Try it again or contact your administrator.",
|
||||
"healthCheckFailed": "The repository health check has failed."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
|
||||
private ConfigDtoToScmConfigurationMapperImpl mapper;
|
||||
|
||||
private final String[] expectedExcludes = {"ex", "clude"};
|
||||
private final String[] expectedUsers = {"trillian", "arthur"};
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
@@ -76,6 +77,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
|
||||
assertEquals("username", config.getNamespaceStrategy());
|
||||
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
|
||||
assertEquals("hitchhiker.mail", config.getMailDomainName());
|
||||
assertTrue("emergencyContacts", config.getEmergencyContacts().containsAll(Arrays.asList(expectedUsers)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,6 +117,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
|
||||
configDto.setNamespaceStrategy("username");
|
||||
configDto.setLoginInfoUrl("https://scm-manager.org/login-info");
|
||||
configDto.setMailDomainName("hitchhiker.mail");
|
||||
configDto.setEmergencyContacts(Sets.newSet(expectedUsers));
|
||||
configDto.setEnabledUserConverter(false);
|
||||
|
||||
return configDto;
|
||||
|
||||
@@ -49,11 +49,10 @@ import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
public class ScmConfigurationToConfigDtoMapperTest {
|
||||
|
||||
private URI baseUri = URI.create("http://example.com/base/");
|
||||
private final URI baseUri = URI.create("http://example.com/base/");
|
||||
|
||||
private String[] expectedUsers = {"trillian", "arthur"};
|
||||
private String[] expectedGroups = {"admin", "plebs"};
|
||||
private String[] expectedExcludes = {"ex", "clude"};
|
||||
private final String[] expectedExcludes = {"ex", "clude"};
|
||||
private final String[] expectedUsers = {"trillian", "arthur"};
|
||||
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -107,6 +106,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
|
||||
assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl());
|
||||
assertEquals("https://www.scm-manager.org/download/rss.xml", dto.getReleaseFeedUrl());
|
||||
assertEquals("scm-manager.local", dto.getMailDomainName());
|
||||
assertTrue("emergencyContacts", dto.getEmergencyContacts().containsAll(Arrays.asList(expectedUsers)));
|
||||
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
|
||||
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
|
||||
@@ -161,6 +161,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
|
||||
config.setNamespaceStrategy("username");
|
||||
config.setLoginInfoUrl("https://scm-manager.org/login-info");
|
||||
config.setReleaseFeedUrl("https://www.scm-manager.org/download/rss.xml");
|
||||
config.setEmergencyContacts(Sets.newSet(expectedUsers));
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
@@ -35,6 +36,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.notifications.NotificationSender;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.FullHealthCheckCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
@@ -47,11 +50,14 @@ import static com.google.common.collect.ImmutableSet.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -74,6 +80,10 @@ class HealthCheckerTest {
|
||||
private RepositoryService repositoryService;
|
||||
@Mock
|
||||
private RepositoryPostProcessor postProcessor;
|
||||
@Mock
|
||||
private ScmConfiguration scmConfiguration;
|
||||
@Mock
|
||||
private NotificationSender notificationSender;
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
@@ -82,7 +92,7 @@ class HealthCheckerTest {
|
||||
|
||||
@BeforeEach
|
||||
void initializeChecker() {
|
||||
this.checker = new HealthChecker(of(healthCheck1, healthCheck2), repositoryManager, repositoryServiceFactory, postProcessor);
|
||||
this.checker = new HealthChecker(of(healthCheck1, healthCheck2), repositoryManager, repositoryServiceFactory, postProcessor, scmConfiguration, notificationSender);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -182,6 +192,7 @@ class HealthCheckerTest {
|
||||
void setUpRepository() {
|
||||
lenient().when(repositoryServiceFactory.create(repository)).thenReturn(repositoryService);
|
||||
lenient().when(repositoryService.getFullCheckCommand()).thenReturn(fullHealthCheckCommand);
|
||||
lenient().when(subject.getPrincipal()).thenReturn("trillian");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -240,6 +251,32 @@ class HealthCheckerTest {
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifyCurrentUserOnlyOnce() throws IOException {
|
||||
when(scmConfiguration.getEmergencyContacts()).thenReturn(ImmutableSet.of("trillian"));
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(repositoryService.isSupported(Command.FULL_HEALTH_CHECK)).thenReturn(true);
|
||||
when(fullHealthCheckCommand.check()).thenReturn(HealthCheckResult.unhealthy(createFailure("error")));
|
||||
|
||||
checker.fullCheck(repositoryId);
|
||||
|
||||
verify(notificationSender, never()).send(any());
|
||||
verify(notificationSender,times(1)).send(any(), eq("trillian"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifyEmergencyContacts() throws IOException {
|
||||
when(scmConfiguration.getEmergencyContacts()).thenReturn(ImmutableSet.of("trillian", "Arthur"));
|
||||
when(healthCheck1.check(repository)).thenReturn(HealthCheckResult.healthy());
|
||||
when(repositoryService.isSupported(Command.FULL_HEALTH_CHECK)).thenReturn(true);
|
||||
when(fullHealthCheckCommand.check()).thenReturn(HealthCheckResult.unhealthy(createFailure("error")));
|
||||
|
||||
checker.fullCheck(repositoryId);
|
||||
|
||||
verify(notificationSender).send(any(), eq("trillian"));
|
||||
verify(notificationSender).send(any(), eq("Arthur"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user