mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-05 07:09:48 +01:00
Merge with default
This commit is contained in:
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@@ -37,7 +37,7 @@ node('docker') {
|
||||
}
|
||||
|
||||
stage('Integration Test') {
|
||||
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true'
|
||||
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -DClassLoaderLeakPreventor.threadWaitMs=10'
|
||||
}
|
||||
|
||||
stage('SonarQube') {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"test": "lerna run --scope '@scm-manager/ui-*' test",
|
||||
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
|
||||
"serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
|
||||
"deploy": "ui-scripts publish"
|
||||
"deploy": "ui-scripts publish",
|
||||
"set-version": "ui-scripts version"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-reflow": "^0.2.7",
|
||||
|
||||
12
pom.xml
12
pom.xml
@@ -220,7 +220,13 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<artifactId>resteasy-core</artifactId>
|
||||
<version>${resteasy.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-core-spi</artifactId>
|
||||
<version>${resteasy.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -831,7 +837,7 @@
|
||||
<servlet.version>3.0.1</servlet.version>
|
||||
|
||||
<jaxrs.version>2.1.1</jaxrs.version>
|
||||
<resteasy.version>3.6.2.Final</resteasy.version>
|
||||
<resteasy.version>4.4.1.Final</resteasy.version>
|
||||
<jersey-client.version>1.19.4</jersey-client.version>
|
||||
<enunciate.version>2.11.1</enunciate.version>
|
||||
<jackson.version>2.10.0</jackson.version>
|
||||
@@ -839,7 +845,7 @@
|
||||
<jaxb.version>2.3.0</jaxb.version>
|
||||
|
||||
<!-- event bus -->
|
||||
<legman.version>1.5.1</legman.version>
|
||||
<legman.version>1.6.1</legman.version>
|
||||
|
||||
<!-- webserver -->
|
||||
<jetty.version>9.4.22.v20191022</jetty.version>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<artifactId>resteasy-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.CopyOnWrite;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
|
||||
@@ -43,7 +44,10 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||
try {
|
||||
Marshaller marshaller = jaxbContext.createMarshaller();
|
||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||
marshaller.marshal(repository, resolveDataPath(path).toFile());
|
||||
CopyOnWrite.withTemporaryFile(
|
||||
temp -> marshaller.marshal(repository, temp.toFile()),
|
||||
resolveDataPath(path)
|
||||
);
|
||||
} catch (JAXBException ex) {
|
||||
throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.store.CopyOnWrite;
|
||||
import sonia.scm.xml.IndentXMLStreamWriter;
|
||||
import sonia.scm.xml.XmlStreams;
|
||||
|
||||
@@ -40,23 +41,28 @@ class PathDatabase {
|
||||
ensureParentDirectoryExists();
|
||||
LOG.trace("write repository path database to {}", storePath);
|
||||
|
||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) {
|
||||
writer.writeStartDocument(ENCODING, VERSION);
|
||||
CopyOnWrite.withTemporaryFile(
|
||||
temp -> {
|
||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
|
||||
writer.writeStartDocument(ENCODING, VERSION);
|
||||
|
||||
writeRepositoriesStart(writer, creationTime, lastModified);
|
||||
for (Map.Entry<String, Path> e : pathDatabase.entrySet()) {
|
||||
writeRepository(writer, e.getKey(), e.getValue());
|
||||
}
|
||||
writer.writeEndElement();
|
||||
writeRepositoriesStart(writer, creationTime, lastModified);
|
||||
for (Map.Entry<String, Path> e : pathDatabase.entrySet()) {
|
||||
writeRepository(writer, e.getKey(), e.getValue());
|
||||
}
|
||||
writer.writeEndElement();
|
||||
|
||||
writer.writeEndDocument();
|
||||
} catch (XMLStreamException | IOException ex) {
|
||||
throw new InternalRepositoryException(
|
||||
ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
|
||||
"failed to write repository path database",
|
||||
ex
|
||||
);
|
||||
}
|
||||
writer.writeEndDocument();
|
||||
} catch (XMLStreamException | IOException ex) {
|
||||
throw new InternalRepositoryException(
|
||||
ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
|
||||
"failed to write repository path database",
|
||||
ex
|
||||
);
|
||||
}
|
||||
},
|
||||
storePath
|
||||
);
|
||||
}
|
||||
|
||||
private void ensureParentDirectoryExists() {
|
||||
|
||||
112
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
112
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class CopyOnWrite {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class);
|
||||
|
||||
private CopyOnWrite() {}
|
||||
|
||||
public static void withTemporaryFile(FileWriter writer, Path targetFile) {
|
||||
validateInput(targetFile);
|
||||
Path temporaryFile = createTemporaryFile(targetFile);
|
||||
executeCallback(writer, targetFile, temporaryFile);
|
||||
replaceOriginalFile(targetFile, temporaryFile);
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:S3725") // performance of Files#isDirectory
|
||||
private static void validateInput(Path targetFile) {
|
||||
if (Files.isDirectory(targetFile)) {
|
||||
throw new IllegalArgumentException("target file has to be a regular file, not a directory");
|
||||
}
|
||||
if (targetFile.getParent() == null) {
|
||||
throw new IllegalArgumentException("target file has to be specified with a parent directory");
|
||||
}
|
||||
}
|
||||
|
||||
private static Path createTemporaryFile(Path targetFile) {
|
||||
Path temporaryFile = targetFile.getParent().resolve(UUID.randomUUID().toString());
|
||||
try {
|
||||
Files.createFile(temporaryFile);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Error creating temporary file {} to replace file {}", temporaryFile, targetFile);
|
||||
throw new StoreException("could not create temporary file", ex);
|
||||
}
|
||||
return temporaryFile;
|
||||
}
|
||||
|
||||
private static void executeCallback(FileWriter writer, Path targetFile, Path temporaryFile) {
|
||||
try {
|
||||
writer.write(temporaryFile);
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error writing to temporary file {}. Target file {} has not been modified", temporaryFile, targetFile);
|
||||
throw new StoreException("could not write temporary file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceOriginalFile(Path targetFile, Path temporaryFile) {
|
||||
Path backupFile = backupOriginalFile(targetFile);
|
||||
try {
|
||||
Files.move(temporaryFile, targetFile);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error renaming temporary file {} to target file {}", temporaryFile, targetFile);
|
||||
restoreBackup(targetFile, backupFile);
|
||||
throw new StoreException("could rename temporary file to target file", e);
|
||||
}
|
||||
deleteBackupFile(backupFile);
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:S3725") // performance of Files#exists
|
||||
private static Path backupOriginalFile(Path targetFile) {
|
||||
Path directory = targetFile.getParent();
|
||||
if (Files.exists(targetFile)) {
|
||||
Path backupFile = directory.resolve(UUID.randomUUID().toString());
|
||||
try {
|
||||
Files.move(targetFile, backupFile);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not backup original file {}. Aborting here so that original file will not be overwritten.", targetFile);
|
||||
throw new StoreException("could not create backup of file", e);
|
||||
}
|
||||
return backupFile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteBackupFile(Path backupFile) {
|
||||
if (backupFile != null) {
|
||||
try {
|
||||
Files.delete(backupFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete backup file {}", backupFile);
|
||||
throw new StoreException("could not delete backup file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreBackup(Path targetFile, Path backupFile) {
|
||||
if (backupFile != null) {
|
||||
try {
|
||||
Files.move(backupFile, targetFile);
|
||||
LOG.info("Recovered original file {} from backup", targetFile);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not replace original file {} with backup file {} after failure", targetFile, backupFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FileWriter {
|
||||
@SuppressWarnings("squid:S00112") // We do not want to limit exceptions here
|
||||
void write(Path t) throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -317,48 +317,47 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
||||
{
|
||||
logger.debug("store configuration to {}", file);
|
||||
|
||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(file))
|
||||
{
|
||||
writer.writeStartDocument();
|
||||
CopyOnWrite.withTemporaryFile(
|
||||
temp -> {
|
||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
|
||||
writer.writeStartDocument();
|
||||
|
||||
// configuration start
|
||||
writer.writeStartElement(TAG_CONFIGURATION);
|
||||
// configuration start
|
||||
writer.writeStartElement(TAG_CONFIGURATION);
|
||||
|
||||
Marshaller m = context.createMarshaller();
|
||||
Marshaller m = context.createMarshaller();
|
||||
|
||||
m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
|
||||
m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
|
||||
|
||||
for (Entry<String, V> e : entries.entrySet())
|
||||
{
|
||||
for (Entry<String, V> e : entries.entrySet()) {
|
||||
|
||||
// entry start
|
||||
writer.writeStartElement(TAG_ENTRY);
|
||||
// entry start
|
||||
writer.writeStartElement(TAG_ENTRY);
|
||||
|
||||
// key start
|
||||
writer.writeStartElement(TAG_KEY);
|
||||
writer.writeCharacters(e.getKey());
|
||||
// key start
|
||||
writer.writeStartElement(TAG_KEY);
|
||||
writer.writeCharacters(e.getKey());
|
||||
|
||||
// key end
|
||||
writer.writeEndElement();
|
||||
// key end
|
||||
writer.writeEndElement();
|
||||
|
||||
// value
|
||||
JAXBElement<V> je = new JAXBElement<V>(QName.valueOf(TAG_VALUE), type,
|
||||
e.getValue());
|
||||
// value
|
||||
JAXBElement<V> je = new JAXBElement<>(QName.valueOf(TAG_VALUE), type,
|
||||
e.getValue());
|
||||
|
||||
m.marshal(je, writer);
|
||||
m.marshal(je, writer);
|
||||
|
||||
// entry end
|
||||
writer.writeEndElement();
|
||||
}
|
||||
// entry end
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
// configuration end
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new StoreException("could not store configuration", ex);
|
||||
}
|
||||
// configuration end
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
}
|
||||
},
|
||||
file.toPath()
|
||||
);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -113,7 +113,10 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
||||
Marshaller marshaller = context.createMarshaller();
|
||||
|
||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||
marshaller.marshal(object, configFile);
|
||||
CopyOnWrite.withTemporaryFile(
|
||||
temp -> marshaller.marshal(object, temp.toFile()),
|
||||
configFile.toPath()
|
||||
);
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new StoreException("failed to marshall object", ex);
|
||||
|
||||
114
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
114
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package sonia.scm.store;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static sonia.scm.store.CopyOnWrite.withTemporaryFile;
|
||||
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class CopyOnWriteTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateNewFile(@TempDirectory.TempDir Path tempDir) {
|
||||
Path expectedFile = tempDir.resolve("toBeCreated.txt");
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOverwriteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
|
||||
Files.createFile(expectedFile);
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||
tempDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForMissingDirectory() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||
Paths.get("someFile")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
|
||||
new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes());
|
||||
|
||||
assertThrows(
|
||||
StoreException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> {
|
||||
throw new IOException("test");
|
||||
},
|
||||
unchangedOriginalFile));
|
||||
|
||||
Assertions.assertThat(unchangedOriginalFile).hasContent("this should be kept");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWrapRuntimeExceptions(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path someFile = tempDir.resolve("something.txt");
|
||||
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
() -> withTemporaryFile(
|
||||
file -> {
|
||||
throw new NullPointerException("test");
|
||||
},
|
||||
someFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
|
||||
new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes());
|
||||
|
||||
assertThrows(
|
||||
StoreException.class,
|
||||
() -> withTemporaryFile(
|
||||
Files::delete,
|
||||
backedUpFile));
|
||||
|
||||
Assertions.assertThat(backedUpFile).hasContent("this should be kept");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
|
||||
new FileOutputStream(expectedFile.toFile()).write("this should be removed".getBytes());
|
||||
|
||||
withTemporaryFile(
|
||||
file -> new FileOutputStream(file.toFile()).write("overwritten".getBytes()),
|
||||
expectedFile);
|
||||
|
||||
Assertions.assertThat(Files.list(tempDir)).hasSize(1);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,13 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<artifactId>resteasy-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-core-spi</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
@@ -209,7 +210,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
||||
RevCommit commit = getLatestCommit(repo, revId, path);
|
||||
|
||||
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk);
|
||||
Optional<LfsPointer> lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk);
|
||||
|
||||
if (lfsPointer.isPresent()) {
|
||||
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||
|
||||
@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -27,6 +24,7 @@ import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.web.GitVndMediaType;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -52,10 +50,7 @@ public class GitConfigResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
|
||||
@@ -89,7 +84,7 @@ public class GitConfigResourceTest {
|
||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory));
|
||||
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
|
||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
||||
dispatcher.addSingletonResource(gitConfigResource);
|
||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||
}
|
||||
|
||||
@@ -137,10 +132,11 @@ public class GitConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:git]");
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = get();
|
||||
|
||||
get();
|
||||
assertEquals("Subject does not have permission [configuration:read:git]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -152,10 +148,11 @@ public class GitConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:git]");
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = put();
|
||||
|
||||
put();
|
||||
assertEquals("Subject does not have permission [configuration:write:git]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -18,12 +15,15 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.web.HgVndMediaType;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
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.Mockito.when;
|
||||
|
||||
@@ -37,10 +37,7 @@ public class HgConfigAutoConfigurationResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@InjectMocks
|
||||
private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper;
|
||||
@@ -57,7 +54,7 @@ public class HgConfigAutoConfigurationResourceTest {
|
||||
new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler);
|
||||
|
||||
when(resourceProvider.get()).thenReturn(resource);
|
||||
dispatcher.getRegistry().addSingletonResource(
|
||||
dispatcher.addSingletonResource(
|
||||
new HgConfigResource(null, null, null, null,
|
||||
resourceProvider, null));
|
||||
}
|
||||
@@ -76,9 +73,10 @@ public class HgConfigAutoConfigurationResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
||||
MockHttpResponse response = put(null);
|
||||
|
||||
put(null);
|
||||
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,9 +93,10 @@ public class HgConfigAutoConfigurationResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
||||
MockHttpResponse response = put("{\"disabled\":true}");
|
||||
|
||||
put("{\"disabled\":true}");
|
||||
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
private MockHttpResponse put(String content) throws URISyntaxException {
|
||||
|
||||
@@ -2,19 +2,17 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -35,10 +33,7 @@ public class HgConfigInstallationsResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
|
||||
@@ -57,7 +52,7 @@ public class HgConfigInstallationsResourceTest {
|
||||
HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper);
|
||||
|
||||
when(resourceProvider.get()).thenReturn(resource);
|
||||
dispatcher.getRegistry().addSingletonResource(
|
||||
dispatcher.addSingletonResource(
|
||||
new HgConfigResource(null, null, null, null,
|
||||
null, resourceProvider));
|
||||
|
||||
@@ -82,9 +77,10 @@ public class HgConfigInstallationsResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
||||
MockHttpResponse response = get("hg");
|
||||
|
||||
get("hg");
|
||||
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,9 +100,10 @@ public class HgConfigInstallationsResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
||||
MockHttpResponse response = get("python");
|
||||
|
||||
get("python");
|
||||
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
private MockHttpResponse get(String path) throws URISyntaxException {
|
||||
|
||||
@@ -5,14 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -23,6 +20,7 @@ import sonia.scm.installer.HgPackageReader;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -49,10 +47,7 @@ public class HgConfigPackageResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = java.net.URI.create("/");
|
||||
|
||||
@@ -113,9 +108,10 @@ public class HgConfigPackageResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetPackagesWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
||||
MockHttpResponse response = get();
|
||||
|
||||
get();
|
||||
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,9 +154,10 @@ public class HgConfigPackageResourceTest {
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotInstallPackageWhenNotAuthorized() throws Exception {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
||||
MockHttpResponse response = put("don-t-care");
|
||||
|
||||
put("don-t-care");
|
||||
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
private List<HgPackage> createPackages() {
|
||||
@@ -191,7 +188,7 @@ public class HgConfigPackageResourceTest {
|
||||
new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper);
|
||||
|
||||
when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource);
|
||||
dispatcher.getRegistry().addSingletonResource(
|
||||
dispatcher.addSingletonResource(
|
||||
new HgConfigResource(null, null, null,
|
||||
hgConfigPackageResourceProvider, null, null));
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -20,6 +17,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.web.HgVndMediaType;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -43,10 +41,7 @@ public class HgConfigResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
|
||||
@@ -78,7 +73,7 @@ public class HgConfigResourceTest {
|
||||
HgConfigResource gitConfigResource =
|
||||
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource,
|
||||
autoconfigResource, installationsResource);
|
||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
||||
dispatcher.addSingletonResource(gitConfigResource);
|
||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||
}
|
||||
|
||||
@@ -120,10 +115,11 @@ public class HgConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = get();
|
||||
|
||||
get();
|
||||
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -135,10 +131,11 @@ public class HgConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = put();
|
||||
|
||||
put();
|
||||
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
private MockHttpResponse get() throws URISyntaxException {
|
||||
|
||||
@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -19,6 +16,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.SvnConfig;
|
||||
import sonia.scm.repository.SvnRepositoryHandler;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.SvnVndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -42,10 +40,7 @@ public class SvnConfigResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
|
||||
@@ -66,7 +61,7 @@ public class SvnConfigResourceTest {
|
||||
SvnConfig gitConfig = createConfiguration();
|
||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||
SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
|
||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
||||
dispatcher.addSingletonResource(gitConfigResource);
|
||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||
}
|
||||
|
||||
@@ -108,10 +103,11 @@ public class SvnConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:svn]");
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = get();
|
||||
|
||||
get();
|
||||
assertEquals("Subject does not have permission [configuration:read:svn]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,10 +119,11 @@ public class SvnConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly")
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:svn]");
|
||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpResponse response = put();
|
||||
|
||||
put();
|
||||
assertEquals("Subject does not have permission [configuration:write:svn]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
private MockHttpResponse get() throws URISyntaxException {
|
||||
|
||||
@@ -42,6 +42,20 @@
|
||||
<version>${mockito.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-core-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||
<version>${resteasy.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
|
||||
111
scm-test/src/main/java/sonia/scm/web/RestDispatcher.java
Normal file
111
scm-test/src/main/java/sonia/scm/web/RestDispatcher.java
Normal file
@@ -0,0 +1,111 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.spi.Dispatcher;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.ScmConstraintViolationException;
|
||||
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.ext.ContextResolver;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RestDispatcher {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RestDispatcher.class);
|
||||
|
||||
private final Dispatcher dispatcher;
|
||||
private final EnhanceableExceptionMapper exceptionMapper;
|
||||
|
||||
public RestDispatcher() {
|
||||
dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
exceptionMapper = new EnhanceableExceptionMapper();
|
||||
dispatcher.getProviderFactory().register(exceptionMapper);
|
||||
dispatcher.getProviderFactory().registerProviderInstance(new JacksonProducer());
|
||||
}
|
||||
|
||||
public void addSingletonResource(Object resource) {
|
||||
dispatcher.getRegistry().addSingletonResource(resource);
|
||||
}
|
||||
|
||||
public void invoke(HttpRequest in, HttpResponse response) {
|
||||
dispatcher.invoke(in, response);
|
||||
}
|
||||
|
||||
public void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
|
||||
exceptionMapper.registerException(exceptionClass, status);
|
||||
}
|
||||
|
||||
public <T> void putDefaultContextObject(Class<T> clazz, T object) {
|
||||
dispatcher.getDefaultContextObjects().put(clazz, object);
|
||||
}
|
||||
|
||||
private static class EnhanceableExceptionMapper implements ExceptionMapper<Exception> {
|
||||
|
||||
private final Map<Class<? extends RuntimeException>, Integer> statusCodes = new HashMap<>();
|
||||
|
||||
public EnhanceableExceptionMapper() {
|
||||
registerException(NotFoundException.class, Status.NOT_FOUND);
|
||||
registerException(AlreadyExistsException.class, Status.CONFLICT);
|
||||
registerException(ConcurrentModificationException.class, Status.CONFLICT);
|
||||
registerException(UnauthorizedException.class, Status.FORBIDDEN);
|
||||
registerException(AuthorizationException.class, Status.FORBIDDEN);
|
||||
registerException(BadRequestException.class, Status.BAD_REQUEST);
|
||||
registerException(ScmConstraintViolationException.class, Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
private void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
|
||||
statusCodes.put(exceptionClass, status.getStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response toResponse(Exception e) {
|
||||
return Response.status(getStatus(e)).entity(e.getMessage()).build();
|
||||
}
|
||||
|
||||
private Integer getStatus(Exception ex) {
|
||||
return statusCodes
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getKey().isAssignableFrom(ex.getClass()))
|
||||
.map(Map.Entry::getValue)
|
||||
.findAny()
|
||||
.orElse(handleUnknownException(ex));
|
||||
}
|
||||
|
||||
private Integer handleUnknownException(Exception ex) {
|
||||
LOG.info("got unknown exception in rest api test", ex);
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
|
||||
@Provider
|
||||
@Produces("application/*+json")
|
||||
public static class JacksonProducer implements ContextResolver<ObjectMapper> {
|
||||
public JacksonProducer() {
|
||||
this.json
|
||||
= new ObjectMapper().findAndRegisterModules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectMapper getContext(Class<?> objectType) {
|
||||
return json;
|
||||
}
|
||||
|
||||
private final ObjectMapper json;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
<name>scm-ui</name>
|
||||
|
||||
<properties>
|
||||
<build.script>build</build.script>
|
||||
<sonar.language>typescript</sonar.language>
|
||||
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
||||
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
||||
@@ -68,7 +69,7 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<script>build</script>
|
||||
<script>${build.script}</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@@ -118,4 +119,21 @@
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
|
||||
<activation>
|
||||
<property>
|
||||
<name>development</name>
|
||||
</property>
|
||||
</activation>
|
||||
|
||||
<properties>
|
||||
<build.script>build:dev</build.script>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"private": false,
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"prettier": "^1.18.2"
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"private": false,
|
||||
"main": "tsconfig.json",
|
||||
"dependencies": {
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"raf": "^3.4.0",
|
||||
"react-test-renderer": "^16.10.2",
|
||||
"storybook-addon-i18next": "^1.2.1",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ActionMeta, ValueType } from "react-select/lib/types";
|
||||
type Props = {
|
||||
loadSuggestions: (p: string) => Promise<SelectValue[]>;
|
||||
valueSelected: (p: SelectValue) => void;
|
||||
label: string;
|
||||
label?: string;
|
||||
helpText?: string;
|
||||
value?: SelectValue;
|
||||
placeholder: string;
|
||||
|
||||
@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
<div
|
||||
className="sc-hzDkRC itNtPz"
|
||||
className="sc-fBuWsC ldmpJA"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
<div
|
||||
className="sc-hzDkRC itNtPz"
|
||||
className="sc-fBuWsC ldmpJA"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
<div
|
||||
className="sc-jhAzac edPAWD"
|
||||
className="sc-fMiknA keSQNk"
|
||||
>
|
||||
<label
|
||||
className="radio"
|
||||
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||
<div
|
||||
className="sc-jhAzac edPAWD"
|
||||
className="sc-fMiknA keSQNk"
|
||||
>
|
||||
<label
|
||||
className="radio"
|
||||
@@ -2311,3 +2311,173 @@ PORT_NUMBER =
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Table|Table Default 1`] = `
|
||||
<table
|
||||
className="sc-jhAzac hmXDXQ table content is-hoverable"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
First Name
|
||||
</th>
|
||||
<th
|
||||
className="has-cursor-pointer"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Last Name
|
||||
<i
|
||||
className="fas fa-sort-amount-down has-text-grey-light sc-hzDkRC escBde"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
E-Mail
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h4>
|
||||
Tricia
|
||||
</h4>
|
||||
</td>
|
||||
<td>
|
||||
<b
|
||||
style={
|
||||
Object {
|
||||
"color": "red",
|
||||
}
|
||||
}
|
||||
>
|
||||
McMillan
|
||||
</b>
|
||||
</td>
|
||||
<td>
|
||||
<a>
|
||||
tricia@hitchhiker.com
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4>
|
||||
Arthur
|
||||
</h4>
|
||||
</td>
|
||||
<td>
|
||||
<b
|
||||
style={
|
||||
Object {
|
||||
"color": "red",
|
||||
}
|
||||
}
|
||||
>
|
||||
Dent
|
||||
</b>
|
||||
</td>
|
||||
<td>
|
||||
<a>
|
||||
arthur@hitchhiker.com
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Table|Table Empty 1`] = `
|
||||
<div
|
||||
className="notification is-info"
|
||||
>
|
||||
|
||||
No data found.
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||
<table
|
||||
className="sc-jhAzac hmXDXQ table content is-hoverable"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className="has-cursor-pointer"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Id
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
className="has-cursor-pointer"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Name
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
className="has-cursor-pointer"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Description
|
||||
<i
|
||||
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
21
|
||||
</td>
|
||||
<td>
|
||||
Pommes
|
||||
</td>
|
||||
<td>
|
||||
Fried potato sticks
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
42
|
||||
</td>
|
||||
<td>
|
||||
Quarter-Pounder
|
||||
</td>
|
||||
<td>
|
||||
Big burger
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
-84
|
||||
</td>
|
||||
<td>
|
||||
Icecream
|
||||
</td>
|
||||
<td>
|
||||
Cold dessert
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
126
scm-ui/ui-components/src/comparators.test.ts
Normal file
126
scm-ui/ui-components/src/comparators.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { byKey, byValueLength, byNestedKeys } from "./comparators";
|
||||
|
||||
const createObject = (key: string, value?: string) => {
|
||||
return {
|
||||
[key]: value
|
||||
};
|
||||
};
|
||||
|
||||
const createObjects = (key: string, values: Array<string | undefined>) => {
|
||||
return values.map(v => createObject(key, v));
|
||||
};
|
||||
|
||||
describe("key comparator tests", () => {
|
||||
it("should sort array", () => {
|
||||
const array = createObjects("key", ["z", "a", "y", "b"]);
|
||||
array.sort(byKey("key"));
|
||||
expect(array).toEqual(createObjects("key", ["a", "b", "y", "z"]));
|
||||
});
|
||||
|
||||
it("should not fail if value is undefined", () => {
|
||||
const array = createObjects("key", ["z", undefined, "a"]);
|
||||
array.sort(byKey("key"));
|
||||
expect(array).toEqual(createObjects("key", ["a", "z", undefined]));
|
||||
});
|
||||
|
||||
it("should not fail if key is undefined", () => {
|
||||
const array = createObjects("key", ["a"]);
|
||||
array.push({});
|
||||
array.push(createObject("key", "z"));
|
||||
array.sort(byKey("key"));
|
||||
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), {}]);
|
||||
});
|
||||
|
||||
it("should not fail if item is undefined", () => {
|
||||
const array: any[] = createObjects("key", ["a"]);
|
||||
array.push(undefined);
|
||||
array.push(createObject("key", "z"));
|
||||
array.sort(byKey("key"));
|
||||
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("length comparator tests", () => {
|
||||
it("should sort array", () => {
|
||||
const array = createObjects("key", ["....", ".", "...", ".."]);
|
||||
array.sort(byValueLength("key"));
|
||||
expect(array).toEqual(createObjects("key", [".", "..", "...", "...."]));
|
||||
});
|
||||
|
||||
it("should not fail if value is undefined", () => {
|
||||
const array = createObjects("key", ["..", undefined, "."]);
|
||||
array.sort(byValueLength("key"));
|
||||
expect(array).toEqual(createObjects("key", [".", "..", undefined]));
|
||||
});
|
||||
|
||||
it("should not fail if key is undefined", () => {
|
||||
const array = createObjects("key", ["."]);
|
||||
array.push({});
|
||||
array.push(createObject("key", ".."));
|
||||
array.sort(byValueLength("key"));
|
||||
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), {}]);
|
||||
});
|
||||
|
||||
it("should not fail if item is undefined", () => {
|
||||
const array: any[] = createObjects("key", ["."]);
|
||||
array.push(undefined);
|
||||
array.push(createObject("key", ".."));
|
||||
array.sort(byValueLength("key"));
|
||||
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nested key comparator tests", () => {
|
||||
const createObject = (key: string, nested?: string, value?: string) => {
|
||||
if (!nested) {
|
||||
return {
|
||||
[key]: undefined
|
||||
};
|
||||
}
|
||||
return {
|
||||
[key]: {
|
||||
[nested]: value
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const createObjects = (key: string, nested: string, values: Array<string | undefined>) => {
|
||||
return values.map(v => createObject(key, nested, v));
|
||||
};
|
||||
|
||||
it("should sort array", () => {
|
||||
const array = createObjects("key", "nested", ["z", "a", "y", "b"]);
|
||||
array.sort(byNestedKeys("key", "nested"));
|
||||
expect(array).toEqual(createObjects("key", "nested", ["a", "b", "y", "z"]));
|
||||
});
|
||||
|
||||
it("should not fail if value is undefined", () => {
|
||||
const array = createObjects("key", "nested", ["z", undefined, "a"]);
|
||||
array.sort(byNestedKeys("key", "nested"));
|
||||
expect(array).toEqual(createObjects("key", "nested", ["a", "z", undefined]));
|
||||
});
|
||||
|
||||
it("should not fail if key is undefined", () => {
|
||||
const array = createObjects("key", "nested", ["a"]);
|
||||
array.push({});
|
||||
array.push(createObject("key", "nested", "z"));
|
||||
array.sort(byNestedKeys("key", "nested"));
|
||||
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), {}]);
|
||||
});
|
||||
|
||||
it("should not fail if nested key is undefined", () => {
|
||||
const array = createObjects("key", "nested", ["a"]);
|
||||
array.push(createObject("key", undefined, "y"));
|
||||
array.push(createObject("key", "nested", "z"));
|
||||
array.sort(byNestedKeys("key", "nested"));
|
||||
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), { key: undefined }]);
|
||||
});
|
||||
|
||||
it("should not fail if item is undefined", () => {
|
||||
const array: any[] = createObjects("key", "nested", ["a"]);
|
||||
array.push(undefined);
|
||||
array.push(createObject("key", "nested", "z"));
|
||||
array.sort(byNestedKeys("key", "nested"));
|
||||
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), undefined]);
|
||||
});
|
||||
});
|
||||
75
scm-ui/ui-components/src/comparators.ts
Normal file
75
scm-ui/ui-components/src/comparators.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
const isUndefined = (o: any, key: string, nested?: string) => {
|
||||
if (typeof o === "undefined" || typeof o[key] === "undefined") {
|
||||
return true;
|
||||
}
|
||||
if (nested) {
|
||||
return typeof o[key][nested] === "undefined";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const byKey = (key: string) => {
|
||||
return (a: any, b: any) => {
|
||||
if (isUndefined(a, key)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isUndefined(b, key)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a[key] < b[key]) {
|
||||
return -1;
|
||||
} else if (a[key] > b[key]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const byValueLength = (key: string) => {
|
||||
return (a: any, b: any) => {
|
||||
if (isUndefined(a, key)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isUndefined(b, key)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a[key].length < b[key].length) {
|
||||
return -1;
|
||||
} else if (a[key].length > b[key].length) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const byNestedKeys = (key: string, nestedKey: string) => {
|
||||
return (a: any, b: any) => {
|
||||
if (isUndefined(a, key, nestedKey)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isUndefined(b, key, nestedKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a[key][nestedKey] < b[key][nestedKey]) {
|
||||
return -1;
|
||||
} else if (a[key][nestedKey] > b[key][nestedKey]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
byKey,
|
||||
byValueLength,
|
||||
byNestedKeys
|
||||
};
|
||||
@@ -52,6 +52,8 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
|
||||
export { default as CardColumnGroup } from "./CardColumnGroup";
|
||||
export { default as CardColumn } from "./CardColumn";
|
||||
|
||||
export { default as comparators } from "./comparators";
|
||||
|
||||
export { apiClient } from "./apiclient";
|
||||
export * from "./errors";
|
||||
|
||||
@@ -63,6 +65,7 @@ export * from "./layout";
|
||||
export * from "./modals";
|
||||
export * from "./navigation";
|
||||
export * from "./repos";
|
||||
export * from "./table";
|
||||
|
||||
export {
|
||||
File,
|
||||
|
||||
18
scm-ui/ui-components/src/table/Column.tsx
Normal file
18
scm-ui/ui-components/src/table/Column.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { ColumnProps } from "./types";
|
||||
|
||||
type Props = ColumnProps & {
|
||||
children: (row: any, columnIndex: number) => ReactNode;
|
||||
};
|
||||
|
||||
const Column: FC<Props> = ({ row, columnIndex, children }) => {
|
||||
if (row === undefined) {
|
||||
throw new Error("missing row, use column only as child of Table");
|
||||
}
|
||||
if (columnIndex === undefined) {
|
||||
throw new Error("missing row, use column only as child of Table");
|
||||
}
|
||||
return <>{children(row, columnIndex)}</>;
|
||||
};
|
||||
|
||||
export default Column;
|
||||
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal file
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import Icon from "../Icon";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
const IconWithMarginLeft = styled(Icon)`
|
||||
visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
|
||||
margin-left: 0.25em;
|
||||
`;
|
||||
|
||||
const SortIcon: FC<Props> = (props: Props) => {
|
||||
return <IconWithMarginLeft name={props.name} isVisible={props.isVisible} />;
|
||||
};
|
||||
|
||||
export default SortIcon;
|
||||
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal file
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Table from "./Table";
|
||||
import Column from "./Column";
|
||||
import TextColumn from "./TextColumn";
|
||||
|
||||
storiesOf("Table|Table", module)
|
||||
.add("Default", () => (
|
||||
<Table
|
||||
data={[
|
||||
{ firstname: "Tricia", lastname: "McMillan", email: "tricia@hitchhiker.com" },
|
||||
{ firstname: "Arthur", lastname: "Dent", email: "arthur@hitchhiker.com" }
|
||||
]}
|
||||
>
|
||||
<Column header={"First Name"}>{(row: any) => <h4>{row.firstname}</h4>}</Column>
|
||||
<Column
|
||||
header={"Last Name"}
|
||||
createComparator={() => {
|
||||
return (a: any, b: any) => {
|
||||
if (a.lastname > b.lastname) {
|
||||
return -1;
|
||||
} else if (a.lastname < b.lastname) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
{(row: any) => <b style={{ color: "red" }}>{row.lastname}</b>}
|
||||
</Column>
|
||||
<Column header={"E-Mail"}>{(row: any) => <a>{row.email}</a>}</Column>
|
||||
</Table>
|
||||
))
|
||||
.add("TextColumn", () => (
|
||||
<Table
|
||||
data={[
|
||||
{ id: "21", title: "Pommes", desc: "Fried potato sticks" },
|
||||
{ id: "42", title: "Quarter-Pounder", desc: "Big burger" },
|
||||
{ id: "-84", title: "Icecream", desc: "Cold dessert" }
|
||||
]}
|
||||
>
|
||||
<TextColumn header="Id" dataKey="id" />
|
||||
<TextColumn header="Name" dataKey="title" />
|
||||
<TextColumn header="Description" dataKey="desc" />
|
||||
</Table>
|
||||
))
|
||||
.add("Empty", () => (
|
||||
<Table data={[]} emptyMessage="No data found.">
|
||||
<TextColumn header="Id" dataKey="id" />
|
||||
<TextColumn header="Name" dataKey="name" />
|
||||
</Table>
|
||||
));
|
||||
120
scm-ui/ui-components/src/table/Table.tsx
Normal file
120
scm-ui/ui-components/src/table/Table.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { FC, ReactElement, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Comparator } from "./types";
|
||||
import SortIcon from "./SortIcon";
|
||||
import Notification from "../Notification";
|
||||
|
||||
const StyledTable = styled.table.attrs(() => ({
|
||||
className: "table content is-hoverable"
|
||||
}))``;
|
||||
|
||||
type Props = {
|
||||
data: any[];
|
||||
sortable?: boolean;
|
||||
emptyMessage?: string;
|
||||
children: Array<ReactElement>;
|
||||
};
|
||||
|
||||
const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
|
||||
const [tableData, setTableData] = useState(data);
|
||||
useEffect(() => {
|
||||
setTableData(data);
|
||||
}, [data]);
|
||||
const [ascending, setAscending] = useState(false);
|
||||
const [lastSortBy, setlastSortBy] = useState<number | undefined>();
|
||||
const [hoveredColumnIndex, setHoveredColumnIndex] = useState<number | undefined>();
|
||||
|
||||
const isSortable = (child: ReactElement) => {
|
||||
return sortable && child.props.createComparator;
|
||||
};
|
||||
|
||||
const sortFunctions: Comparator | undefined[] = [];
|
||||
React.Children.forEach(children, (child, index) => {
|
||||
if (child && isSortable(child)) {
|
||||
sortFunctions.push(child.props.createComparator(child.props, index));
|
||||
} else {
|
||||
sortFunctions.push(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const mapDataToColumns = (row: any) => {
|
||||
return (
|
||||
<tr>
|
||||
{React.Children.map(children, (child, columnIndex) => {
|
||||
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const sortDescending = (sortAscending: (a: any, b: any) => number) => {
|
||||
return (a: any, b: any) => {
|
||||
return sortAscending(a, b) * -1;
|
||||
};
|
||||
};
|
||||
|
||||
const tableSort = (index: number) => {
|
||||
const sortFn = sortFunctions[index];
|
||||
if (!sortFn) {
|
||||
throw new Error(`column with index ${index} is not sortable`);
|
||||
}
|
||||
const sortableData = [...tableData];
|
||||
let sortOrder = ascending;
|
||||
if (lastSortBy !== index) {
|
||||
setAscending(true);
|
||||
sortOrder = true;
|
||||
}
|
||||
const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
|
||||
sortableData.sort(sortFunction);
|
||||
setTableData(sortableData);
|
||||
setAscending(!sortOrder);
|
||||
setlastSortBy(index);
|
||||
};
|
||||
|
||||
const shouldShowIcon = (index: number) => {
|
||||
return index === lastSortBy || index === hoveredColumnIndex;
|
||||
};
|
||||
|
||||
if (!tableData || tableData.length <= 0) {
|
||||
if (emptyMessage) {
|
||||
return <Notification type="info">{emptyMessage}</Notification>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
{React.Children.map(children, (child, index) => (
|
||||
<th
|
||||
className={isSortable(child) && "has-cursor-pointer"}
|
||||
onClick={isSortable(child) ? () => tableSort(index) : undefined}
|
||||
onMouseEnter={() => setHoveredColumnIndex(index)}
|
||||
onMouseLeave={() => setHoveredColumnIndex(undefined)}
|
||||
>
|
||||
{child.props.header}
|
||||
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{tableData.map(mapDataToColumns)}</tbody>
|
||||
</StyledTable>
|
||||
);
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
sortable: true
|
||||
};
|
||||
|
||||
const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
|
||||
if (child.props.ascendingIcon && child.props.descendingIcon) {
|
||||
return <SortIcon name={ascending ? child.props.ascendingIcon : child.props.descendingIcon} isVisible={showIcon} />;
|
||||
} else {
|
||||
return <SortIcon name={ascending ? "sort-amount-down-alt" : "sort-amount-down"} isVisible={showIcon} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default Table;
|
||||
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal file
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { FC } from "react";
|
||||
import { ColumnProps } from "./types";
|
||||
import comparators from "../comparators";
|
||||
|
||||
type Props = ColumnProps & {
|
||||
dataKey: string;
|
||||
};
|
||||
|
||||
const TextColumn: FC<Props> = ({ row, dataKey }) => {
|
||||
return row[dataKey];
|
||||
};
|
||||
|
||||
TextColumn.defaultProps = {
|
||||
createComparator: (props: Props) => {
|
||||
return comparators.byKey(props.dataKey);
|
||||
},
|
||||
ascendingIcon: "sort-alpha-down-alt",
|
||||
descendingIcon: "sort-alpha-down"
|
||||
};
|
||||
|
||||
export default TextColumn;
|
||||
4
scm-ui/ui-components/src/table/index.ts
Normal file
4
scm-ui/ui-components/src/table/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Table } from "./Table";
|
||||
export { default as Column } from "./Column";
|
||||
export { default as TextColumn } from "./TextColumn";
|
||||
export { default as SortIcon } from "./SortIcon";
|
||||
12
scm-ui/ui-components/src/table/types.ts
Normal file
12
scm-ui/ui-components/src/table/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export type Comparator = (a: any, b: any) => number;
|
||||
|
||||
export type ColumnProps = {
|
||||
header: ReactNode;
|
||||
row?: any;
|
||||
columnIndex?: number;
|
||||
createComparator?: (props: any, columnIndex: number) => Comparator;
|
||||
ascendingIcon?: string;
|
||||
descendingIcon?: string;
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/react": "^16.9.9",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const commands = ["plugin", "plugin-watch", "publish"];
|
||||
const commands = ["plugin", "plugin-watch", "publish", "version"];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const versions = require("../versions");
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.log("usage ui-scripts publish version");
|
||||
console.log("usage ui-scripts publish <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
12
scm-ui/ui-scripts/src/commands/version.js
Normal file
12
scm-ui/ui-scripts/src/commands/version.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const lerna = require("../lerna");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.log("usage ui-scripts version <new-version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = args[0];
|
||||
|
||||
lerna.version(version);
|
||||
@@ -26,6 +26,6 @@
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/jest": "^24.0.19",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
"systemjs": "0.21.6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"flow": "flow"
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<artifactId>resteasy-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -154,26 +154,38 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-validator-provider-11</artifactId>
|
||||
<artifactId>resteasy-validator-provider</artifactId>
|
||||
<version>${resteasy.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>5.3.6.Final</version>
|
||||
<version>6.1.0.Final</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.el</groupId>
|
||||
<artifactId>javax.el-api</artifactId>
|
||||
<version>2.2.4</version>
|
||||
<version>3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.web</groupId>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.el</artifactId>
|
||||
<version>2.2.4</version>
|
||||
<version>3.0.1-b11</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- injection -->
|
||||
|
||||
@@ -60,7 +60,6 @@ public class SourceRootResource {
|
||||
if (revision != null && !revision.isEmpty()) {
|
||||
browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8"));
|
||||
}
|
||||
browseCommand.setDisableCache(true);
|
||||
BrowserResult browserResult = browseCommand.getBrowserResult();
|
||||
|
||||
if (browserResult != null) {
|
||||
|
||||
@@ -90,8 +90,12 @@ public class LegmanScmEventBus extends ScmEventBus
|
||||
@Override
|
||||
public void post(Object event)
|
||||
{
|
||||
logger.debug("post {} to event bus {}", event, name);
|
||||
eventBus.post(event);
|
||||
if (eventBus != null) {
|
||||
logger.debug("post {} to event bus {}", event, name);
|
||||
eventBus.post(event);
|
||||
} else {
|
||||
logger.error("failed to post event {}, because event bus is shutdown", event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,9 +108,12 @@ public class LegmanScmEventBus extends ScmEventBus
|
||||
@Override
|
||||
public void register(Object object)
|
||||
{
|
||||
logger.trace("register {} to event bus {}", object, name);
|
||||
eventBus.register(object);
|
||||
|
||||
if (eventBus != null) {
|
||||
logger.trace("register {} to event bus {}", object, name);
|
||||
eventBus.register(object);
|
||||
} else {
|
||||
logger.error("failed to register {}, because eventbus is shutdown", object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,22 +125,37 @@ public class LegmanScmEventBus extends ScmEventBus
|
||||
@Override
|
||||
public void unregister(Object object)
|
||||
{
|
||||
logger.trace("unregister {} from event bus {}", object, name);
|
||||
|
||||
try
|
||||
{
|
||||
eventBus.unregister(object);
|
||||
if (eventBus != null) {
|
||||
logger.trace("unregister {} from event bus {}", object, name);
|
||||
|
||||
try {
|
||||
eventBus.unregister(object);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
logger.trace("object {} was not registered", object);
|
||||
}
|
||||
} else {
|
||||
logger.error("failed to unregister object {}, because event bus is shutdown", object);
|
||||
}
|
||||
catch (IllegalArgumentException ex)
|
||||
{
|
||||
logger.trace("object {} was not registered", object);
|
||||
}
|
||||
|
||||
@Subscribe(async = false)
|
||||
public void shutdownEventBus(ShutdownEventBusEvent shutdownEventBusEvent) {
|
||||
if (eventBus != null) {
|
||||
logger.info("shutdown event bus executor for {}, because of received ShutdownEventBusEvent", name);
|
||||
eventBus.shutdown();
|
||||
eventBus = null;
|
||||
} else {
|
||||
logger.warn("event bus was already shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(async = false)
|
||||
public void recreateEventBus(RecreateEventBusEvent recreateEventBusEvent) {
|
||||
logger.info("shutdown event bus executor for {}", name);
|
||||
eventBus.shutdown();
|
||||
if (eventBus != null) {
|
||||
logger.info("shutdown event bus executor for {}, because of received RecreateEventBusEvent", name);
|
||||
eventBus.shutdown();
|
||||
}
|
||||
logger.info("recreate event bus because of received RecreateEventBusEvent");
|
||||
eventBus = create();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package sonia.scm.event;
|
||||
|
||||
public class ShutdownEventBusEvent {
|
||||
}
|
||||
@@ -58,13 +58,16 @@ public class BootstrapContextFilter extends GuiceFilter {
|
||||
|
||||
private final BootstrapContextListener listener = new BootstrapContextListener();
|
||||
|
||||
private ClassLoader webAppClassLoader;
|
||||
|
||||
/** Field description */
|
||||
private FilterConfig filterConfig;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
this.filterConfig = filterConfig;
|
||||
|
||||
// store webapp classloader for delayed restarts
|
||||
webAppClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
initializeContext();
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ public class BootstrapContextFilter extends GuiceFilter {
|
||||
if (filterConfig == null) {
|
||||
LOG.error("filter config is null, scm-manager is not initialized");
|
||||
} else {
|
||||
RestartStrategy restartStrategy = RestartStrategy.get();
|
||||
RestartStrategy restartStrategy = RestartStrategy.get(webAppClassLoader);
|
||||
restartStrategy.restart(new GuiceInjectionContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class BootstrapContextListener extends GuiceServletContextListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextListener.class);
|
||||
|
||||
private final ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
||||
private ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
||||
|
||||
private ServletContext context;
|
||||
private InjectionLifeCycle injectionLifeCycle;
|
||||
@@ -88,14 +88,16 @@ public class BootstrapContextListener extends GuiceServletContextListener {
|
||||
protected Injector getInjector() {
|
||||
Throwable startupError = SCMContext.getContext().getStartupError();
|
||||
if (startupError != null) {
|
||||
LOG.error("received unrecoverable error during startup", startupError);
|
||||
return createStageOneInjector(SingleView.error(startupError));
|
||||
} else if (Versions.isTooOld()) {
|
||||
LOG.error("Existing version is too old and cannot be migrated to new version. Please update to version {} first", Versions.MIN_VERSION);
|
||||
LOG.error("existing version is too old and cannot be migrated to new version. Please update to version {} first", Versions.MIN_VERSION);
|
||||
return createStageOneInjector(SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT));
|
||||
} else {
|
||||
try {
|
||||
return createStageTwoInjector();
|
||||
} catch (Exception ex) {
|
||||
LOG.error("failed to create stage two injector", ex);
|
||||
return createStageOneInjector(SingleView.error(ex));
|
||||
}
|
||||
}
|
||||
@@ -110,6 +112,8 @@ public class BootstrapContextListener extends GuiceServletContextListener {
|
||||
injectionLifeCycle.shutdown();
|
||||
injectionLifeCycle = null;
|
||||
classLoaderLifeCycle.shutdown();
|
||||
|
||||
super.contextDestroyed(sce);
|
||||
}
|
||||
|
||||
private Injector createStageTwoInjector() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.event.RecreateEventBusEvent;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.event.ShutdownEventBusEvent;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -13,20 +14,47 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
*/
|
||||
public class InjectionContextRestartStrategy implements RestartStrategy {
|
||||
|
||||
private static final String DISABLE_RESTART_PROPERTY = "sonia.scm.restart.disable";
|
||||
private static final String WAIT_PROPERTY = "sonia.scm.restart.wait";
|
||||
private static final String DISABLE_GC_PROPERTY = "sonia.scm.restart.disable-gc";
|
||||
|
||||
private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class);
|
||||
|
||||
private long waitInMs = 250L;
|
||||
private boolean restartEnabled = !Boolean.getBoolean(DISABLE_RESTART_PROPERTY);
|
||||
private long waitInMs = Integer.getInteger(WAIT_PROPERTY, 250);
|
||||
private boolean gcEnabled = !Boolean.getBoolean(DISABLE_GC_PROPERTY);
|
||||
|
||||
private final ClassLoader webAppClassLoader;
|
||||
|
||||
InjectionContextRestartStrategy(ClassLoader webAppClassLoader) {
|
||||
this.webAppClassLoader = webAppClassLoader;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setWaitInMs(long waitInMs) {
|
||||
this.waitInMs = waitInMs;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setGcEnabled(boolean gcEnabled) {
|
||||
this.gcEnabled = gcEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restart(InjectionContext context) {
|
||||
LOG.warn("destroy injection context");
|
||||
context.destroy();
|
||||
stop(context);
|
||||
if (restartEnabled) {
|
||||
start(context);
|
||||
} else {
|
||||
LOG.warn("restarting context is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:S1215") // suppress explicit gc call warning
|
||||
private void start(InjectionContext context) {
|
||||
LOG.debug("use WebAppClassLoader as ContextClassLoader, to avoid ClassLoader leaks");
|
||||
Thread.currentThread().setContextClassLoader(webAppClassLoader);
|
||||
|
||||
LOG.warn("send recreate eventbus event");
|
||||
ScmEventBus.getInstance().post(new RecreateEventBusEvent());
|
||||
@@ -34,6 +62,12 @@ public class InjectionContextRestartStrategy implements RestartStrategy {
|
||||
// restart context delayed, to avoid timing problems
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (gcEnabled){
|
||||
LOG.info("call gc to clean up memory from old instances");
|
||||
System.gc();
|
||||
}
|
||||
|
||||
LOG.info("wait {}ms before re starting the context", waitInMs);
|
||||
Thread.sleep(waitInMs);
|
||||
|
||||
LOG.warn("reinitialize injection context");
|
||||
@@ -45,6 +79,15 @@ public class InjectionContextRestartStrategy implements RestartStrategy {
|
||||
LOG.error("failed to restart", ex);
|
||||
}
|
||||
}, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start();
|
||||
}
|
||||
|
||||
private void stop(InjectionContext context) {
|
||||
LOG.warn("destroy injection context");
|
||||
context.destroy();
|
||||
|
||||
if (!restartEnabled) {
|
||||
// shutdown eventbus, but do this only if restart is disabled
|
||||
ScmEventBus.getInstance().post(new ShutdownEventBusEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public interface RestartStrategy {
|
||||
* Initialize the injection context.
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Destroys the injection context.
|
||||
*/
|
||||
@@ -31,8 +30,8 @@ public interface RestartStrategy {
|
||||
*
|
||||
* @return configured strategy
|
||||
*/
|
||||
static RestartStrategy get() {
|
||||
return new InjectionContextRestartStrategy();
|
||||
static RestartStrategy get(ClassLoader webAppClassLoader) {
|
||||
return new InjectionContextRestartStrategy(webAppClassLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,29 @@ package sonia.scm.lifecycle.classloading;
|
||||
* find it in a heap dump.
|
||||
*/
|
||||
class BootstrapClassLoader extends ClassLoader {
|
||||
|
||||
/**
|
||||
* Marker to find a BootstrapClassLoader, which is already shutdown.
|
||||
*/
|
||||
private boolean shutdown = false;
|
||||
|
||||
BootstrapClassLoader(ClassLoader webappClassLoader) {
|
||||
super(webappClassLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the classloader was shutdown.
|
||||
*
|
||||
* @return {@code true} if the classloader was shutdown
|
||||
*/
|
||||
boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the class loader as shutdown.
|
||||
*/
|
||||
void markAsShutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
|
||||
import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp;
|
||||
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
|
||||
import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp;
|
||||
import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp;
|
||||
import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator;
|
||||
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator;
|
||||
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator;
|
||||
import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator;
|
||||
import sonia.scm.lifecycle.LifeCycle;
|
||||
import sonia.scm.plugin.ChildFirstPluginClassLoader;
|
||||
@@ -16,9 +22,9 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT;
|
||||
|
||||
/**
|
||||
* Creates and shutdown SCM-Manager ClassLoaders.
|
||||
@@ -27,23 +33,25 @@ public final class ClassLoaderLifeCycle implements LifeCycle {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
|
||||
|
||||
private final Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
|
||||
private Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
|
||||
|
||||
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
|
||||
private final ClassLoader webappClassLoader;
|
||||
|
||||
private ClassLoader bootstrapClassLoader;
|
||||
private UnaryOperator<ClassLoader> classLoaderAppendListener = c -> c;
|
||||
private BootstrapClassLoader bootstrapClassLoader;
|
||||
|
||||
private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() {
|
||||
@Override
|
||||
public <C extends ClassLoader> C apply(C classLoader) {
|
||||
return classLoader;
|
||||
}
|
||||
};
|
||||
|
||||
@VisibleForTesting
|
||||
public static ClassLoaderLifeCycle create() {
|
||||
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory();
|
||||
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
|
||||
// the SunAwtAppContextInitiator causes a lot of exceptions and we use no awt
|
||||
classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class);
|
||||
// the MBeanCleanUp causes a Exception and we use no mbeans
|
||||
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
|
||||
return new ClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader(), classLoaderLeakPreventorFactory);
|
||||
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
|
||||
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = createClassLoaderLeakPreventorFactory(webappClassLoader);
|
||||
return new ClassLoaderLifeCycle(webappClassLoader, classLoaderLeakPreventorFactory);
|
||||
}
|
||||
|
||||
ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
|
||||
@@ -51,12 +59,64 @@ public final class ClassLoaderLifeCycle implements LifeCycle {
|
||||
this.webappClassLoader = initAndAppend(webappClassLoader);
|
||||
}
|
||||
|
||||
private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) {
|
||||
// Should threads tied to the web app classloader be forced to stop at application shutdown?
|
||||
boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads");
|
||||
|
||||
// Should Timer threads tied to the web app classloader be forced to stop at application shutdown?
|
||||
boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads");
|
||||
|
||||
// Should shutdown hooks registered from the application be executed at application shutdown?
|
||||
boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks");
|
||||
|
||||
// No of milliseconds to wait for threads to finish execution, before stopping them.
|
||||
int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT);
|
||||
|
||||
/*
|
||||
* No of milliseconds to wait for shutdown hooks to finish execution, before stopping them.
|
||||
* If set to -1 there will be no waiting at all, but Thread is allowed to run until finished.
|
||||
*/
|
||||
int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT);
|
||||
|
||||
LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycle.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) );
|
||||
LOG.info(" stopThreads = {}", stopThreads);
|
||||
LOG.info(" stopTimerThreads = {}", stopTimerThreads);
|
||||
LOG.info(" executeShutdownHooks = {}", executeShutdownHooks);
|
||||
LOG.info(" threadWaitMs = {} ms", threadWaitMs);
|
||||
LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs);
|
||||
|
||||
// use webapp classloader as safe base? or system?
|
||||
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader);
|
||||
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
|
||||
|
||||
final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class);
|
||||
shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks);
|
||||
shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs);
|
||||
|
||||
final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class);
|
||||
stopThreadsCleanUp.setStopThreads(stopThreads);
|
||||
stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads);
|
||||
stopThreadsCleanUp.setThreadWaitMs(threadWaitMs);
|
||||
|
||||
// remove awt and imageio cleanup
|
||||
classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class);
|
||||
classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class);
|
||||
classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class);
|
||||
classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class);
|
||||
classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class);
|
||||
|
||||
// the MBeanCleanUp causes a Exception and we use no mbeans
|
||||
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
|
||||
|
||||
return classLoaderLeakPreventorFactory;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setClassLoaderAppendListener(UnaryOperator<ClassLoader> classLoaderAppendListener) {
|
||||
void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) {
|
||||
this.classLoaderAppendListener = classLoaderAppendListener;
|
||||
}
|
||||
|
||||
@@ -84,12 +144,17 @@ public final class ClassLoaderLifeCycle implements LifeCycle {
|
||||
clap.shutdown();
|
||||
clap = classLoaders.poll();
|
||||
}
|
||||
// be sure it is realy empty
|
||||
classLoaders.clear();
|
||||
classLoaders = new ArrayDeque<>();
|
||||
|
||||
bootstrapClassLoader.markAsShutdown();
|
||||
bootstrapClassLoader = null;
|
||||
}
|
||||
|
||||
private ClassLoader initAndAppend(ClassLoader originalClassLoader) {
|
||||
private <T extends ClassLoader> T initAndAppend(T originalClassLoader) {
|
||||
LOG.debug("init classloader {}", originalClassLoader);
|
||||
ClassLoader classLoader = classLoaderAppendListener.apply(originalClassLoader);
|
||||
T classLoader = classLoaderAppendListener.apply(originalClassLoader);
|
||||
|
||||
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
|
||||
preventor.runPreClassLoaderInitiators();
|
||||
@@ -98,6 +163,10 @@ public final class ClassLoaderLifeCycle implements LifeCycle {
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
interface ClassLoaderAppendListener {
|
||||
<C extends ClassLoader> C apply(C classLoader);
|
||||
}
|
||||
|
||||
private class ClassLoaderAndPreventor {
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.jboss.resteasy.plugins.server.servlet.ListenerBootstrap;
|
||||
import org.jboss.resteasy.spi.Registry;
|
||||
import org.jboss.resteasy.spi.ResteasyDeployment;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.jboss.resteasy.spi.statistics.StatisticsController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -70,10 +71,21 @@ public class ResteasyAllInOneServletDispatcher extends HttpServletDispatcher {
|
||||
super.destroy();
|
||||
deployment.stop();
|
||||
|
||||
// clear ResourceLocatorInvoker leaks
|
||||
StatisticsController statisticsController = ResteasyProviderFactory.getInstance().getStatisticsController();
|
||||
if (statisticsController != null) {
|
||||
statisticsController.reset();
|
||||
}
|
||||
|
||||
// ensure everything gets cleared, to avoid classloader leaks
|
||||
ResteasyProviderFactory.clearInstanceIfEqual(ResteasyProviderFactory.getInstance());
|
||||
ResteasyProviderFactory.clearContextData();
|
||||
RuntimeDelegate.setInstance(null);
|
||||
|
||||
removeDeploymentFromServletContext();
|
||||
}
|
||||
|
||||
private void removeDeploymentFromServletContext() {
|
||||
getServletContext().removeAttribute(ResteasyDeployment.class.getName());
|
||||
}
|
||||
|
||||
private ResteasyDeployment getDeploymentFromServletContext() {
|
||||
|
||||
@@ -17,15 +17,17 @@ public class CronScheduler implements Scheduler {
|
||||
|
||||
private final ScheduledExecutorService executorService;
|
||||
private final CronTaskFactory taskFactory;
|
||||
private final CronThreadFactory threadFactory;
|
||||
|
||||
@Inject
|
||||
public CronScheduler(CronTaskFactory taskFactory) {
|
||||
this.taskFactory = taskFactory;
|
||||
this.threadFactory = new CronThreadFactory();
|
||||
this.executorService = createExecutor();
|
||||
}
|
||||
|
||||
private ScheduledExecutorService createExecutor() {
|
||||
return Executors.newScheduledThreadPool(2, new CronThreadFactory());
|
||||
return Executors.newScheduledThreadPool(2, threadFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,6 +54,7 @@ public class CronScheduler implements Scheduler {
|
||||
@Override
|
||||
public void close() {
|
||||
LOG.debug("shutdown underlying executor service");
|
||||
threadFactory.close();
|
||||
executorService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.schedule;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -19,7 +20,10 @@ class CronThreadFactory implements ThreadFactory, AutoCloseable {
|
||||
|
||||
private static final AtomicLong FACTORY_COUNTER = new AtomicLong();
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor(
|
||||
new ThreadFactoryBuilder().setNameFormat("CronThreadFactory-%d").build()
|
||||
);
|
||||
|
||||
private final long factoryId = FACTORY_COUNTER.incrementAndGet();
|
||||
private final AtomicLong threadCounter = new AtomicLong();
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class DefaultCGIExecutorFactory implements CGIExecutorFactory
|
||||
public class DefaultCGIExecutorFactory implements CGIExecutorFactory, AutoCloseable
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -92,6 +92,11 @@ public class DefaultCGIExecutorFactory implements CGIExecutorFactory
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
/** Field description */
|
||||
private final ExecutorService executor;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package sonia.scm.api.rest;
|
||||
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AuthorizationExceptionMapperTest {
|
||||
|
||||
private final Subject subject = mock(Subject.class);
|
||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapNormalUserToForbidden() {
|
||||
when(subject.getPrincipal()).thenReturn("someone");
|
||||
|
||||
assertThat(
|
||||
new AuthorizationExceptionMapper().toResponse(new AuthorizationException()).getStatus()
|
||||
).isEqualTo(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapAnonymousUserToUnauthorized() {
|
||||
when(subject.getPrincipal()).thenReturn("_anonymous");
|
||||
|
||||
assertThat(
|
||||
new AuthorizationExceptionMapper().toResponse(new AuthorizationException()).getStatus()
|
||||
).isEqualTo(401);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -19,18 +16,17 @@ import sonia.scm.security.AccessTokenBuilder;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.net.URI.create;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -47,7 +43,7 @@ public class AuthenticationResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
@@ -116,7 +112,7 @@ public class AuthenticationResourceTest {
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
authenticationResource = new AuthenticationResource(accessTokenBuilderFactory, cookieIssuer);
|
||||
dispatcher.getRegistry().addSingletonResource(authenticationResource);
|
||||
dispatcher.addSingletonResource(authenticationResource);
|
||||
|
||||
AccessToken accessToken = mock(AccessToken.class);
|
||||
when(accessToken.getExpiration()).thenReturn(new Date(Long.MAX_VALUE));
|
||||
@@ -125,10 +121,9 @@ public class AuthenticationResourceTest {
|
||||
when(accessTokenBuilderFactory.create()).thenReturn(accessTokenBuilder);
|
||||
|
||||
HttpServletRequest servletRequest = mock(HttpServletRequest.class);
|
||||
ResteasyProviderFactory.getContextDataMap().put(HttpServletRequest.class, servletRequest);
|
||||
|
||||
dispatcher.putDefaultContextObject(HttpServletRequest.class, servletRequest);
|
||||
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
|
||||
ResteasyProviderFactory.getContextDataMap().put(HttpServletResponse.class, servletResponse);
|
||||
dispatcher.putDefaultContextObject(HttpServletResponse.class, servletResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -23,6 +22,7 @@ import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.user.DefaultUserDisplayManager;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.xml.XmlUserDAO;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
import sonia.scm.xml.XmlDatabase;
|
||||
|
||||
@@ -39,7 +39,6 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/shiro-002.ini")
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
@@ -50,7 +49,8 @@ public class AutoCompleteResourceTest {
|
||||
|
||||
public static final String URL = "/" + AutoCompleteResource.PATH;
|
||||
private final Integer defaultLimit = DisplayManager.DEFAULT_LIMIT;
|
||||
private Dispatcher dispatcher;
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private XmlUserDAO userDao;
|
||||
private XmlGroupDAO groupDao;
|
||||
@@ -74,7 +74,7 @@ public class AutoCompleteResourceTest {
|
||||
DefaultUserDisplayManager userManager = new DefaultUserDisplayManager(this.userDao);
|
||||
DefaultGroupDisplayManager groupManager = new DefaultGroupDisplayManager(groupDao);
|
||||
AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager);
|
||||
dispatcher = createDispatcher(autoCompleteResource);
|
||||
dispatcher.addSingletonResource(autoCompleteResource);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.apache.shiro.ShiroException;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.jboss.resteasy.spi.UnhandledException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -24,6 +21,7 @@ import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||
import sonia.scm.plugin.PluginCondition;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Provider;
|
||||
@@ -34,7 +32,7 @@ import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@@ -46,7 +44,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AvailablePluginResourceTest {
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
Provider<AvailablePluginResource> availablePluginResourceProvider;
|
||||
@@ -71,10 +69,9 @@ class AvailablePluginResourceTest {
|
||||
|
||||
@BeforeEach
|
||||
void prepareEnvironment() {
|
||||
dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
pluginRootResource = new PluginRootResource(null, availablePluginResourceProvider, null);
|
||||
when(availablePluginResourceProvider.get()).thenReturn(availablePluginResource);
|
||||
dispatcher.getRegistry().addSingletonResource(pluginRootResource);
|
||||
dispatcher.addSingletonResource(pluginRootResource);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -195,20 +192,23 @@ class AvailablePluginResourceTest {
|
||||
@BeforeEach
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
doThrow(new ShiroException()).when(subject).checkPermission(any(String.class));
|
||||
doThrow(new UnauthorizedException()).when(subject).checkPermission(any(String.class));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotGetAvailablePluginsIfMissingPermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available");
|
||||
request.accept(VndMediaType.PLUGIN_COLLECTION);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response));
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
verify(subject).checkPermission(any(String.class));
|
||||
}
|
||||
|
||||
@@ -218,7 +218,9 @@ class AvailablePluginResourceTest {
|
||||
request.accept(VndMediaType.PLUGIN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response));
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
verify(subject).checkPermission(any(String.class));
|
||||
}
|
||||
|
||||
@@ -228,7 +230,9 @@ class AvailablePluginResourceTest {
|
||||
request.accept(VndMediaType.PLUGIN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response));
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
verify(subject).checkPermission(any(String.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -30,8 +29,10 @@ import sonia.scm.repository.api.BranchesCommandBuilder;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
@@ -54,7 +55,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
public static final String BRANCH_PATH = "space/repo/branches/master";
|
||||
public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH;
|
||||
public static final String REVISION = "revision";
|
||||
private Dispatcher dispatcher;
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -101,7 +103,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
|
||||
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks);
|
||||
super.branchRootResource = Providers.of(branchRootResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
@@ -129,7 +131,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(404, response.getStatus());
|
||||
assertEquals("application/vnd.scmm-error+json;v=2", response.getOutputHeaders().getFirst("Content-Type"));
|
||||
MediaType contentType = (MediaType) response.getOutputHeaders().getFirst("Content-Type");
|
||||
Assertions.assertThat(response.getContentAsString()).contains("branch", "master", "space/repo");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -26,6 +25,7 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -48,7 +48,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
|
||||
public static final String CHANGESET_PATH = "space/repo/changesets/";
|
||||
public static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH;
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -79,7 +79,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase {
|
||||
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper);
|
||||
super.changesetRootResource = Providers.of(changesetRootResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
|
||||
@@ -4,18 +4,16 @@ import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.io.Resources;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
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.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -41,10 +39,7 @@ public class ConfigResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
@@ -71,7 +66,7 @@ public class ConfigResourceTest {
|
||||
|
||||
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator);
|
||||
|
||||
dispatcher.getRegistry().addSingletonResource(configResource);
|
||||
dispatcher.addSingletonResource(configResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,13 +83,14 @@ public class ConfigResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "writeOnly")
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
thrown.expectMessage("Subject does not have permission [configuration:read:global]");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals("Subject does not have permission [configuration:read:global]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,9 +116,10 @@ public class ConfigResourceTest {
|
||||
MockHttpRequest request = post("sonia/scm/api/v2/config-test-update.json");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
thrown.expectMessage("Subject does not have permission [configuration:write:global]");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals("Subject does not have permission [configuration:write:global]", response.getContentAsString());
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -18,16 +16,17 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||
import sonia.scm.api.v2.NotFoundExceptionMapper;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.CRLFInjectionException;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -46,7 +45,8 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String DIFF_PATH = "space/repo/diff/";
|
||||
public static final String DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + DIFF_PATH;
|
||||
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@@ -68,14 +68,12 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void prepareEnvironment() {
|
||||
diffRootResource = new DiffRootResource(serviceFactory);
|
||||
super.diffRootResource = Providers.of(diffRootResource);
|
||||
dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl();
|
||||
dispatcher.getProviderFactory().register(new NotFoundExceptionMapper(mapper));
|
||||
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class);
|
||||
dispatcher.registerException(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
|
||||
when(service.getDiffCommand()).thenReturn(diffCommandBuilder);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
|
||||
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||
import sonia.scm.api.rest.BadRequestExceptionMapper;
|
||||
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
|
||||
import sonia.scm.api.v2.NotFoundExceptionMapper;
|
||||
|
||||
public class DispatcherMock {
|
||||
public static Dispatcher createDispatcher(Object resource) {
|
||||
Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
dispatcher.getRegistry().addSingletonResource(resource);
|
||||
ExceptionWithContextToErrorDtoMapperImpl mapper = new ExceptionWithContextToErrorDtoMapperImpl();
|
||||
dispatcher.getProviderFactory().register(new NotFoundExceptionMapper(mapper));
|
||||
dispatcher.getProviderFactory().register(new AlreadyExistsExceptionMapper(mapper));
|
||||
dispatcher.getProviderFactory().register(new ConcurrentModificationExceptionMapper(mapper));
|
||||
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().register(new InternalRepositoryExceptionMapper(mapper));
|
||||
dispatcher.getProviderFactory().register(new BadRequestExceptionMapper(mapper));
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -28,6 +27,7 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -70,7 +70,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase {
|
||||
|
||||
private FileHistoryRootResource fileHistoryRootResource;
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final Subject subject = mock(Subject.class);
|
||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||
@@ -80,7 +80,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase {
|
||||
fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper);
|
||||
super.fileHistoryRootResource = Providers.of(fileHistoryRootResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -15,12 +14,11 @@ import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.api.rest.JSONContextResolver;
|
||||
import sonia.scm.api.rest.ObjectMapperProvider;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -43,7 +41,6 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -55,7 +52,7 @@ public class GroupRootResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||
|
||||
@@ -91,8 +88,7 @@ public class GroupRootResourceTest {
|
||||
GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper, groupPermissionResource);
|
||||
GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource));
|
||||
|
||||
dispatcher = createDispatcher(groupRootResource);
|
||||
dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get()));
|
||||
dispatcher.addSingletonResource(groupRootResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -28,8 +27,11 @@ import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.CRLFInjectionException;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
@@ -53,7 +55,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
public static final String INCOMING_CHANGESETS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||
public static final String INCOMING_DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -88,13 +90,13 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper);
|
||||
super.incomingRootResource = Providers.of(incomingRootResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder);
|
||||
when(repositoryService.getDiffCommand()).thenReturn(diffCommandBuilder);
|
||||
dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class);
|
||||
dispatcher.registerException(CRLFInjectionException.class, Response.Status.BAD_REQUEST);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.jboss.resteasy.spi.UnhandledException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -17,9 +15,9 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Provider;
|
||||
@@ -31,15 +29,17 @@ import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.plugin.PluginTestHelper.createInstalled;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InstalledPluginResourceTest {
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
Provider<InstalledPluginResource> installedPluginResourceProvider;
|
||||
@@ -65,10 +65,9 @@ class InstalledPluginResourceTest {
|
||||
|
||||
@BeforeEach
|
||||
void prepareEnvironment() {
|
||||
dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
pluginRootResource = new PluginRootResource(installedPluginResourceProvider, null, null);
|
||||
when(installedPluginResourceProvider.get()).thenReturn(installedPluginResource);
|
||||
dispatcher.getRegistry().addSingletonResource(pluginRootResource);
|
||||
dispatcher.addSingletonResource(pluginRootResource);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -77,7 +76,6 @@ class InstalledPluginResourceTest {
|
||||
@BeforeEach
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -129,7 +127,13 @@ class InstalledPluginResourceTest {
|
||||
class WithoutAuthorization {
|
||||
|
||||
@BeforeEach
|
||||
void unbindSubject() {
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
doThrow(new UnauthorizedException()).when(subject).checkPermission(any(String.class));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@@ -139,7 +143,9 @@ class InstalledPluginResourceTest {
|
||||
request.accept(VndMediaType.PLUGIN_COLLECTION);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response));
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,7 +154,9 @@ class InstalledPluginResourceTest {
|
||||
request.accept(VndMediaType.PLUGIN);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response));
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -21,6 +20,7 @@ import sonia.scm.group.GroupCollector;
|
||||
import sonia.scm.user.InvalidPasswordException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -37,7 +37,6 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -49,9 +48,10 @@ public class MeResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||
|
||||
@Mock
|
||||
private ScmPathInfo uriInfo;
|
||||
@Mock
|
||||
@@ -85,7 +85,7 @@ public class MeResourceTest {
|
||||
MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService);
|
||||
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
dispatcher = createDispatcher(meResource);
|
||||
dispatcher.addSingletonResource(meResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -24,6 +23,7 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ModificationsCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -45,7 +45,7 @@ public class ModificationsResourceTest extends RepositoryTestBase {
|
||||
public static final String MODIFICATIONS_PATH = "space/repo/modifications/";
|
||||
public static final String MODIFICATIONS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + MODIFICATIONS_PATH;
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -73,7 +73,7 @@ public class ModificationsResourceTest extends RepositoryTestBase {
|
||||
public void prepareEnvironment() {
|
||||
modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper);
|
||||
super.modificationsRootResource = Providers.of(modificationsRootResource);
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
|
||||
@@ -4,8 +4,6 @@ import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.ShiroException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -22,10 +20,10 @@ import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
@@ -42,7 +40,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PendingPluginResourceTest {
|
||||
|
||||
Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/"));
|
||||
|
||||
@@ -61,10 +59,9 @@ class PendingPluginResourceTest {
|
||||
|
||||
@BeforeEach
|
||||
void prepareEnvironment() {
|
||||
dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
dispatcher.getProviderFactory().register(new PermissionExceptionMapper());
|
||||
dispatcher.registerException(ShiroException.class, Response.Status.UNAUTHORIZED);
|
||||
PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource));
|
||||
dispatcher.getRegistry().addSingletonResource(pluginRootResource);
|
||||
dispatcher.addSingletonResource(pluginRootResource);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -207,14 +204,6 @@ class PendingPluginResourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class PermissionExceptionMapper implements ExceptionMapper<ShiroException> {
|
||||
|
||||
@Override
|
||||
public Response toResponse(ShiroException exception) {
|
||||
return Response.status(401).entity(exception.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private AvailablePlugin createAvailablePlugin(String name) {
|
||||
PluginInformation pluginInformation = new PluginInformation();
|
||||
pluginInformation.setName(name);
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
@@ -30,6 +29,7 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
@@ -58,7 +58,6 @@ import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
|
||||
|
||||
@Slf4j
|
||||
@@ -105,7 +104,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
|
||||
.content(PERMISSION_TEST_PAYLOAD)
|
||||
.path(PATH_OF_ONE_PERMISSION);
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
@@ -133,7 +132,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
|
||||
repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
|
||||
repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
|
||||
super.permissionRootResource = Providers.of(repositoryPermissionRootResource);
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
@@ -180,19 +179,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
|
||||
requestPUTPermission.expectedResponseStatus(403));
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
@DisplayName("test endpoints on missing permissions and is _anonymous")
|
||||
Stream<DynamicTest> missedPermissionAnonymousUnauthorizedTestFactory() {
|
||||
when(subject.getPrincipal()).thenReturn("_anonymous");
|
||||
doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class));
|
||||
return createDynamicTestsToAssertResponses(
|
||||
requestGETPermission.expectedResponseStatus(401),
|
||||
requestPOSTPermission.expectedResponseStatus(401),
|
||||
requestGETAllPermissions.expectedResponseStatus(401),
|
||||
requestDELETEPermission.expectedResponseStatus(401),
|
||||
requestPUTPermission.expectedResponseStatus(401));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userWithPermissionWritePermissionShouldGetAllPermissionsWithCreateAndUpdateLinks() throws URISyntaxException {
|
||||
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
|
||||
|
||||
@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -16,10 +15,9 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.api.rest.JSONContextResolver;
|
||||
import sonia.scm.api.rest.ObjectMapperProvider;
|
||||
import sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -36,7 +34,6 @@ import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -66,7 +63,7 @@ public class RepositoryRoleRootResourceTest {
|
||||
|
||||
private RepositoryRoleCollectionToDtoMapper collectionToDtoMapper;
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<RepositoryRole> modifyCaptor;
|
||||
@@ -87,8 +84,7 @@ public class RepositoryRoleRootResourceTest {
|
||||
when(repositoryRoleManager.create(createCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
|
||||
doNothing().when(repositoryRoleManager).delete(deleteCaptor.capture());
|
||||
|
||||
dispatcher = createDispatcher(rootResource);
|
||||
dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get()));
|
||||
dispatcher.addSingletonResource(rootResource);
|
||||
|
||||
when(repositoryRoleManager.get(CUSTOM_ROLE)).thenReturn(CUSTOM_REPOSITORY_ROLE);
|
||||
when(repositoryRoleManager.get(SYSTEM_ROLE)).thenReturn(SYSTEM_REPOSITORY_ROLE);
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -23,6 +22,7 @@ import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -47,12 +47,10 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -63,7 +61,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
private static final String REALM = "AdminRealm";
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
@@ -98,7 +96,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
super.manager = repositoryManager;
|
||||
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
|
||||
super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks));
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
|
||||
|
||||
@@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -15,6 +13,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -32,7 +31,7 @@ import static org.mockito.Mockito.when;
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class RepositoryTypeRootResourceTest {
|
||||
|
||||
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
@@ -56,7 +55,7 @@ public class RepositoryTypeRootResourceTest {
|
||||
RepositoryTypeCollectionResource collectionResource = new RepositoryTypeCollectionResource(repositoryManager, collectionMapper);
|
||||
RepositoryTypeResource resource = new RepositoryTypeResource(repositoryManager, mapper);
|
||||
RepositoryTypeRootResource rootResource = new RepositoryTypeRootResource(Providers.of(collectionResource), Providers.of(resource));
|
||||
dispatcher.getRegistry().addSingletonResource(rootResource);
|
||||
dispatcher.addSingletonResource(rootResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -18,6 +17,7 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.BrowseCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -25,13 +25,12 @@ import java.net.URISyntaxException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@@ -58,7 +57,7 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper);
|
||||
super.sourceRootResource = Providers.of(sourceRootResource);
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.apache.shiro.subject.support.SubjectThreadState;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.apache.shiro.util.ThreadState;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.After;
|
||||
@@ -17,8 +16,6 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||
import sonia.scm.api.v2.NotFoundExceptionMapper;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.Tag;
|
||||
@@ -26,6 +23,7 @@ import sonia.scm.repository.Tags;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.api.TagsCommandBuilder;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -36,7 +34,6 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@Slf4j
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
@@ -44,7 +41,8 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String TAG_PATH = "space/repo/tags/";
|
||||
public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH;
|
||||
private Dispatcher dispatcher ;
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -74,12 +72,10 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
||||
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
|
||||
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper);
|
||||
super.tagRootResource = Providers.of(tagRootResource);
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
|
||||
when(repositoryService.getTagsCommand()).thenReturn(tagsCommandBuilder);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
@@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -12,7 +10,12 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.plugin.*;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginResources;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -24,14 +27,17 @@ import java.util.HashSet;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.hamcrest.Matchers.equalToIgnoringCase;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
public class UIRootResourceTest {
|
||||
|
||||
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
@Mock
|
||||
private PluginLoader pluginLoader;
|
||||
@@ -50,7 +56,7 @@ public class UIRootResourceTest {
|
||||
UIPluginResource pluginResource = new UIPluginResource(pluginLoader, collectionMapper, mapper);
|
||||
UIRootResource rootResource = new UIRootResource(Providers.of(pluginResource));
|
||||
|
||||
dispatcher.getRegistry().addSingletonResource(rootResource);
|
||||
dispatcher.addSingletonResource(rootResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
@@ -23,6 +22,7 @@ import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -45,7 +45,6 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@SubjectAware(
|
||||
username = "trillian",
|
||||
@@ -57,7 +56,7 @@ public class UserRootResourceTest {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||
|
||||
@@ -99,7 +98,7 @@ public class UserRootResourceTest {
|
||||
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
|
||||
Providers.of(userResource));
|
||||
|
||||
dispatcher = createDispatcher(userRootResource);
|
||||
dispatcher.addSingletonResource(userRootResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,11 +18,13 @@ class InjectionContextRestartStrategyTest {
|
||||
@Mock
|
||||
private RestartStrategy.InjectionContext context;
|
||||
|
||||
private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy();
|
||||
private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(Thread.currentThread().getContextClassLoader());
|
||||
|
||||
@BeforeEach
|
||||
void setWaitToZero() {
|
||||
strategy.setWaitInMs(0L);
|
||||
// disable gc during tests
|
||||
strategy.setGcEnabled(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -47,7 +49,6 @@ class InjectionContextRestartStrategyTest {
|
||||
@Test
|
||||
void shouldRegisterContextAfterRestart() throws InterruptedException {
|
||||
TestingInjectionContext ctx = new TestingInjectionContext();
|
||||
|
||||
strategy.restart(ctx);
|
||||
|
||||
Thread.sleep(50L);
|
||||
|
||||
@@ -70,7 +70,12 @@ class ClassLoaderLifeCycleTest {
|
||||
URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()));
|
||||
|
||||
ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(webappClassLoader);
|
||||
lifeCycle.setClassLoaderAppendListener(c -> spy(c));
|
||||
lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycle.ClassLoaderAppendListener() {
|
||||
@Override
|
||||
public <C extends ClassLoader> C apply(C classLoader) {
|
||||
return spy(classLoader);
|
||||
}
|
||||
});
|
||||
lifeCycle.initialize();
|
||||
|
||||
ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -11692,6 +11692,11 @@ prettier@^1.16.4, prettier@^1.18.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
|
||||
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
|
||||
|
||||
prettier@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
pretty-error@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
|
||||
@@ -14430,11 +14435,16 @@ typescript-compiler@^1.4.1-2:
|
||||
resolved "https://registry.yarnpkg.com/typescript-compiler/-/typescript-compiler-1.4.1-2.tgz#ba4f7db22d91534a1929d90009dce161eb72fd3f"
|
||||
integrity sha1-uk99si2RU0oZKdkACdzhYety/T8=
|
||||
|
||||
typescript@^3.4, typescript@^3.6.4:
|
||||
typescript@^3.4:
|
||||
version "3.6.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
|
||||
integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
|
||||
|
||||
typescript@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
||||
integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.20"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"
|
||||
|
||||
Reference in New Issue
Block a user