mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01:00
Show hg binary verification error messages (#1637)
Show hg verification error messages on global hg config page if trying to save invalid hg binary.
This commit is contained in:
2
gradle/changelog/hg_verification_error_messages.yaml
Normal file
2
gradle/changelog/hg_verification_error_messages.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Show hg binary verification error messages ([#1637](https://github.com/scm-manager/scm-manager/pull/1637))
|
||||||
@@ -35,6 +35,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import sonia.scm.config.ConfigurationPermissions;
|
import sonia.scm.config.ConfigurationPermissions;
|
||||||
import sonia.scm.repository.HgGlobalConfig;
|
import sonia.scm.repository.HgGlobalConfig;
|
||||||
import sonia.scm.repository.HgRepositoryHandler;
|
import sonia.scm.repository.HgRepositoryHandler;
|
||||||
|
import sonia.scm.repository.HgVerifier;
|
||||||
import sonia.scm.web.HgVndMediaType;
|
import sonia.scm.web.HgVndMediaType;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ import javax.ws.rs.Path;
|
|||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RESTful Web Service Resource to manage the configuration of the hg plugin.
|
* RESTful Web Service Resource to manage the configuration of the hg plugin.
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +147,7 @@ public class HgConfigResource {
|
|||||||
responseCode = "204",
|
responseCode = "204",
|
||||||
description = "update success"
|
description = "update success"
|
||||||
)
|
)
|
||||||
|
@ApiResponse(responseCode = "400", description = "invalid configuration")
|
||||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||||
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege")
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"configuration:write:hg\" privilege")
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
@@ -154,11 +158,16 @@ public class HgConfigResource {
|
|||||||
schema = @Schema(implementation = ErrorDto.class)
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
))
|
))
|
||||||
public Response update(@Valid HgGlobalGlobalConfigDto configDto) {
|
public Response update(@Valid HgGlobalGlobalConfigDto configDto) {
|
||||||
|
|
||||||
HgGlobalConfig config = dtoToConfigMapper.map(configDto);
|
HgGlobalConfig config = dtoToConfigMapper.map(configDto);
|
||||||
|
|
||||||
ConfigurationPermissions.write(config).check();
|
ConfigurationPermissions.write(config).check();
|
||||||
|
|
||||||
|
if (config.getHgBinary() != null) {
|
||||||
|
HgVerifier.HgVerifyStatus verifyStatus = new HgVerifier().verify(config.getHgBinary());
|
||||||
|
doThrow()
|
||||||
|
.violation(verifyStatus.getDescription())
|
||||||
|
.when(verifyStatus != HgVerifier.HgVerifyStatus.VALID);
|
||||||
|
}
|
||||||
|
|
||||||
repositoryHandler.setConfig(config);
|
repositoryHandler.setConfig(config);
|
||||||
repositoryHandler.storeConfig();
|
repositoryHandler.storeConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
import sonia.scm.io.SimpleCommand;
|
import sonia.scm.io.SimpleCommand;
|
||||||
import sonia.scm.io.SimpleCommandResult;
|
import sonia.scm.io.SimpleCommandResult;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlEnum;
|
||||||
|
import javax.xml.bind.annotation.XmlEnumValue;
|
||||||
|
import javax.xml.bind.annotation.XmlType;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -48,23 +51,31 @@ public class HgVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid(HgGlobalConfig config) {
|
public boolean isValid(HgGlobalConfig config) {
|
||||||
return isValid(config.getHgBinary());
|
return verify(config) == HgVerifyStatus.VALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid(String hg) {
|
public boolean isValid(Path path) {
|
||||||
return isValid(Paths.get(hg));
|
return verify(path) == HgVerifyStatus.VALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid(Path hg) {
|
public HgVerifyStatus verify(HgGlobalConfig config) {
|
||||||
|
return verify(config.getHgBinary());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HgVerifyStatus verify(String hg) {
|
||||||
|
return verify(Paths.get(hg));
|
||||||
|
}
|
||||||
|
|
||||||
|
public HgVerifyStatus verify(Path hg) {
|
||||||
LOG.trace("check if hg binary {} is valid", hg);
|
LOG.trace("check if hg binary {} is valid", hg);
|
||||||
if (!Files.isRegularFile(hg)) {
|
if (!Files.isRegularFile(hg)) {
|
||||||
LOG.warn("{} is not a regular file", hg);
|
LOG.warn("{} is not a regular file", hg);
|
||||||
return false;
|
return HgVerifyStatus.NOT_REGULAR_FILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Files.isExecutable(hg)) {
|
if (!Files.isExecutable(hg)) {
|
||||||
LOG.warn("{} is not executable", hg);
|
LOG.warn("{} is not executable", hg);
|
||||||
return false;
|
return HgVerifyStatus.NOT_EXECUTABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -72,31 +83,31 @@ public class HgVerifier {
|
|||||||
return isVersionValid(hg, version);
|
return isVersionValid(hg, version);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LOG.warn("failed to resolve version of {}: ", hg, ex);
|
LOG.warn("failed to resolve version of {}: ", hg, ex);
|
||||||
return false;
|
return HgVerifyStatus.COULD_NOT_RESOLVE_VERSION;
|
||||||
} catch (InterruptedException ex) {
|
} catch (InterruptedException ex) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
LOG.warn("failed to resolve version of {}: ", hg, ex);
|
LOG.warn("failed to resolve version of {}: ", hg, ex);
|
||||||
return false;
|
return HgVerifyStatus.COULD_NOT_RESOLVE_VERSION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isVersionValid(Path hg, String version) {
|
private HgVerifyStatus isVersionValid(Path hg, String version) {
|
||||||
String[] parts = version.split("\\.");
|
String[] parts = version.split("\\.");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
LOG.warn("{} returned invalid version: {}", hg, version);
|
LOG.warn("{} returned invalid version: {}", hg, version);
|
||||||
return false;
|
return HgVerifyStatus.INVALID_VERSION;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
int major = Integer.parseInt(parts[0]);
|
int major = Integer.parseInt(parts[0]);
|
||||||
if (major < 4) {
|
if (major < 4) {
|
||||||
LOG.warn("{} is too old, we need at least mercurial 4.x", hg);
|
LOG.warn("{} is too old, we need at least mercurial 4.x", hg);
|
||||||
return false;
|
return HgVerifyStatus.VERSION_TOO_OLD;
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
LOG.warn("{} returned invalid version {}", hg, version);
|
LOG.warn("{} returned invalid version {}", hg, version);
|
||||||
return false;
|
return HgVerifyStatus.INVALID_VERSION;
|
||||||
}
|
}
|
||||||
return true;
|
return HgVerifyStatus.VALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VersionResolver defaultVersionResolver() {
|
private VersionResolver defaultVersionResolver() {
|
||||||
@@ -115,4 +126,22 @@ public class HgVerifier {
|
|||||||
String resolveVersion(Path hg) throws IOException, InterruptedException;
|
String resolveVersion(Path hg) throws IOException, InterruptedException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HgVerifyStatus {
|
||||||
|
VALID("hg binary is valid"),
|
||||||
|
NOT_REGULAR_FILE("hg binary is not a regular file"),
|
||||||
|
NOT_EXECUTABLE("hg binary is not executable"),
|
||||||
|
INVALID_VERSION("hg binary returned invalid version"),
|
||||||
|
VERSION_TOO_OLD("hg binary version is too old, we need at least 4.x"),
|
||||||
|
COULD_NOT_RESOLVE_VERSION("failed to resolve version of hg binary");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
HgVerifyStatus(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,13 @@ public class HgConfigResourceTest {
|
|||||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "writeOnly")
|
||||||
|
public void shouldNotUpdateConfigForInvalidBinary() throws URISyntaxException {
|
||||||
|
MockHttpResponse response = put("{\"hgBinary\":\"3.2.1\"}");
|
||||||
|
assertEquals(400, response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
@@ -172,9 +179,13 @@ public class HgConfigResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MockHttpResponse put() throws URISyntaxException {
|
private MockHttpResponse put() throws URISyntaxException {
|
||||||
|
return put("{\"disabled\":true}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockHttpResponse put(String config) throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2)
|
MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2)
|
||||||
.contentType(HgVndMediaType.CONFIG)
|
.contentType(HgVndMediaType.CONFIG)
|
||||||
.content("{\"disabled\":true}".getBytes());
|
.content(config.getBytes());
|
||||||
|
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|||||||
@@ -51,22 +51,22 @@ class HgVerifierTest {
|
|||||||
private final HgVerifier verifier = new HgVerifier();
|
private final HgVerifier verifier = new HgVerifier();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnFalseIfFileDoesNotExists(@TempDir Path directory) {
|
void shouldReturnNotRegularFileIfFileDoesNotExists(@TempDir Path directory) {
|
||||||
Path hg = directory.resolve("hg");
|
Path hg = directory.resolve("hg");
|
||||||
|
|
||||||
boolean isValid = verify(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.NOT_REGULAR_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnFalseIfFileIsADirectory(@TempDir Path directory) throws IOException {
|
void shouldReturnNotRegularFileIfFileIsADirectory(@TempDir Path directory) throws IOException {
|
||||||
Path hg = directory.resolve("hg");
|
Path hg = directory.resolve("hg");
|
||||||
Files.createDirectories(hg);
|
Files.createDirectories(hg);
|
||||||
|
|
||||||
boolean isValid = verify(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.NOT_REGULAR_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -74,21 +74,21 @@ class HgVerifierTest {
|
|||||||
Path hg = directory.resolve("hg");
|
Path hg = directory.resolve("hg");
|
||||||
Files.createFile(hg);
|
Files.createFile(hg);
|
||||||
|
|
||||||
boolean isValid = verify(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.NOT_EXECUTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnTrueForIfMercurialIsAvailable() {
|
void shouldReturnValidIfMercurialIsAvailable() {
|
||||||
Path hg = findHg();
|
Path hg = findHg();
|
||||||
|
|
||||||
// skip test if we could not find mercurial
|
// skip test if we could not find mercurial
|
||||||
Assumptions.assumeTrue(hg != null, "skip test, because we could not find mercurial on path");
|
Assumptions.assumeTrue(hg != null, "skip test, because we could not find mercurial on path");
|
||||||
|
|
||||||
boolean isValid = verify(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isTrue();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.VALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path findHg() {
|
private Path findHg() {
|
||||||
@@ -106,48 +106,59 @@ class HgVerifierTest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean verify(Path hg) {
|
private HgVerifier.HgVerifyStatus verify(Path hg) {
|
||||||
HgGlobalConfig config = new HgGlobalConfig();
|
HgGlobalConfig config = new HgGlobalConfig();
|
||||||
config.setHgBinary(hg.toString());
|
config.setHgBinary(hg.toString());
|
||||||
return verifier.isValid(config);
|
return verifier.verify(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = { "3-2-1", "x.y.z", "3.2.0" })
|
@ValueSource(strings = { "3-2-1", "x.y.z" })
|
||||||
void shouldReturnFalseForInvalidVersions(String version, @TempDir Path directory) throws IOException {
|
void shouldReturnInvalidVersions(String version, @TempDir Path directory) throws IOException {
|
||||||
HgVerifier verifier = new HgVerifier(hg -> version);
|
HgVerifier verifier = new HgVerifier(hg -> version);
|
||||||
|
|
||||||
Path hg = createHg(directory);
|
Path hg = createHg(directory);
|
||||||
|
|
||||||
boolean isValid = verifier.isValid(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verifier.verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.INVALID_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnFalseOnIOException(@TempDir Path directory) throws IOException {
|
void shouldReturnTooOldVersion(@TempDir Path directory) throws IOException {
|
||||||
|
HgVerifier verifier = new HgVerifier(hg -> "3.2.1");
|
||||||
|
|
||||||
|
Path hg = createHg(directory);
|
||||||
|
|
||||||
|
HgVerifier.HgVerifyStatus verifyStatus = verifier.verify(hg);
|
||||||
|
|
||||||
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.VERSION_TOO_OLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnCouldNotResolveOnIOException(@TempDir Path directory) throws IOException {
|
||||||
HgVerifier verifier = new HgVerifier(hg -> {
|
HgVerifier verifier = new HgVerifier(hg -> {
|
||||||
throw new IOException("failed");
|
throw new IOException("failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
Path hg = createHg(directory);
|
Path hg = createHg(directory);
|
||||||
|
|
||||||
boolean isValid = verifier.isValid(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verifier.verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isFalse();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.COULD_NOT_RESOLVE_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnTrue(@TempDir Path directory) throws IOException {
|
void shouldReturnValid(@TempDir Path directory) throws IOException {
|
||||||
HgVerifier verifier = new HgVerifier(hg -> "4.2.0");
|
HgVerifier verifier = new HgVerifier(hg -> "4.2.0");
|
||||||
|
|
||||||
Path hg = createHg(directory);
|
Path hg = createHg(directory);
|
||||||
|
|
||||||
boolean isValid = verifier.isValid(hg);
|
HgVerifier.HgVerifyStatus verifyStatus = verifier.verify(hg);
|
||||||
|
|
||||||
assertThat(isValid).isTrue();
|
assertThat(verifyStatus).isEqualTo(HgVerifier.HgVerifyStatus.VALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ class Configuration extends React.Component<Props, State> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
modifying: true
|
modifying: true,
|
||||||
|
error: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const { modifiedConfiguration } = this.state;
|
const { modifiedConfiguration } = this.state;
|
||||||
@@ -182,9 +183,7 @@ class Configuration extends React.Component<Props, State> {
|
|||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const { fetching, error, configuration, modifying, valid } = this.state;
|
const { fetching, error, configuration, modifying, valid } = this.state;
|
||||||
|
|
||||||
if (error) {
|
if (fetching || !configuration) {
|
||||||
return <ErrorNotification error={error} />;
|
|
||||||
} else if (fetching || !configuration) {
|
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
} else {
|
} else {
|
||||||
const readOnly = this.isReadOnly();
|
const readOnly = this.isReadOnly();
|
||||||
@@ -200,6 +199,12 @@ class Configuration extends React.Component<Props, State> {
|
|||||||
{this.renderConfigChangedNotification()}
|
{this.renderConfigChangedNotification()}
|
||||||
<form onSubmit={this.modifyConfiguration}>
|
<form onSubmit={this.modifyConfiguration}>
|
||||||
{this.props.render(renderProps)}
|
{this.props.render(renderProps)}
|
||||||
|
{error && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<ErrorNotification error={error} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
<Level
|
<Level
|
||||||
right={<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />}
|
right={<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user