diff --git a/pom.xml b/pom.xml index 5e67c117db..3295e732db 100644 --- a/pom.xml +++ b/pom.xml @@ -305,7 +305,7 @@ org.mockito - mockito-all + mockito-core ${mockito.version} test diff --git a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java index f7b4d75fbd..270b996635 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java @@ -33,24 +33,19 @@ package sonia.scm.repository.api; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Preconditions; import com.google.common.base.Strings; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.spi.CatCommand; import sonia.scm.repository.spi.CatCommandRequest; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** @@ -106,23 +101,31 @@ public final class CatCommandBuilder } /** - * Passes the content of the given file to the outputstream. + * Passes the content of the given file to the output stream. * - * @param outputStream outputstream for the content + * @param outputStream output stream for the content * @param path file path - * - * @return {@code this} - * - * @throws IOException - * @throws RepositoryException */ - public CatCommandBuilder retriveContent(OutputStream outputStream, - String path) - throws IOException, RepositoryException - { + public void retriveContent(OutputStream outputStream, String path) throws IOException, RepositoryException { getCatResult(outputStream, path); + } - return this; + /** + * Returns an output stream with the file content. + * + * @param path file path + */ + public InputStream getStream(String path) throws IOException, RepositoryException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(path), + "path is required"); + + CatCommandRequest requestClone = request.clone(); + + requestClone.setPath(path); + + logger.debug("create cat stream for {}", requestClone); + + return catCommand.getCatResultStream(requestClone); } //~--- get methods ---------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java index b9a6f52ab0..e0e336f2ff 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java @@ -33,13 +33,10 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import sonia.scm.repository.RepositoryException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; /** @@ -47,19 +44,9 @@ import java.io.OutputStream; * @author Sebastian Sdorra * @since 1.17 */ -public interface CatCommand -{ +public interface CatCommand { - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException; + void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException; + + InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException; } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java new file mode 100644 index 0000000000..a461e40dea --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -0,0 +1,67 @@ +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.Collection; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; +import static sonia.scm.it.RestUtil.given; +import static sonia.scm.it.ScmTypes.availableScmTypes; + +@RunWith(Parameterized.class) +public class RepositoryAccessITCase { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private final String repositoryType; + private RepositoryUtil repositoryUtil; + + public RepositoryAccessITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void initClient() throws IOException { + TestData.createDefault(); + repositoryUtil = new RepositoryUtil(repositoryType, tempFolder.getRoot()); + } + + @Test + public void shouldFindBranches() throws IOException { + assumeFalse("There are no branches for SVN", repositoryType.equals("svn")); + + repositoryUtil.createAndCommitFile("a.txt", "a"); + + String branchesUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.branches.href"); + + Object branchName = given() + .when() + .get(branchesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.branches[0].name"); + + assertNotNull(branchName); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java new file mode 100644 index 0000000000..98d4c8cdab --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -0,0 +1,62 @@ +package sonia.scm.it; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.apache.http.HttpStatus; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Person; +import sonia.scm.repository.client.api.ClientCommand; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientFactory; +import sonia.scm.web.VndMediaType; + +import java.io.File; +import java.io.IOException; + +import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.RestUtil.ADMIN_USERNAME; +import static sonia.scm.it.RestUtil.given; + +public class RepositoryUtil { + + private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); + + private final RepositoryClient repositoryClient; + private final File folder; + + RepositoryUtil(String repositoryType, File folder) throws IOException { + this.repositoryClient = createRepositoryClient(repositoryType, folder); + this.folder = folder; + } + + static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { + String httpProtocolUrl = given(VndMediaType.REPOSITORY) + + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.httpProtocol.href"); + + + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, ADMIN_USERNAME, ADMIN_PASSWORD, folder); + } + + void createAndCommitFile(String fileName, String content) throws IOException { + Files.write(content, new File(folder, fileName), Charsets.UTF_8); + repositoryClient.getAddCommand().add(fileName); + commit("added " + fileName); + } + + Changeset commit(String message) throws IOException { + Changeset changeset = repositoryClient.getCommitCommand().commit( + new Person("scmadmin", "scmadmin@scm-manager.org"), message + ); + if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { + repositoryClient.getPushCommand().push(); + } + return changeset; + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java index 1458ab6d0b..76d2461637 100644 --- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java @@ -16,10 +16,18 @@ public class RestUtil { return REST_BASE_URL.resolve(path); } + public static final String ADMIN_USERNAME = "scmadmin"; + public static final String ADMIN_PASSWORD = "scmadmin"; + public static RequestSpecification given(String mediaType) { return RestAssured.given() .contentType(mediaType) .accept(mediaType) - .auth().preemptive().basic("scmadmin", "scmadmin"); + .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); + } + + public static RequestSpecification given() { + return RestAssured.given() + .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 4420b9a22a..c451dd8614 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -32,158 +32,133 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Strings; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.GitUtil; import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.Closeable; +import java.io.FilterInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; -/** - * - * @author Sebastian Sdorra - */ -public class GitCatCommand extends AbstractGitCommand implements CatCommand -{ - /** - * the logger for GitCatCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(GitCatCommand.class); +public class GitCatCommand extends AbstractGitCommand implements CatCommand { - //~--- constructors --------------------------------------------------------- + private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class); - /** - * Constructs ... - * - * - * - * @param context - * @param repository - */ - public GitCatCommand(GitContext context, - sonia.scm.repository.Repository repository) - { + public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { + public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException { logger.debug("try to read content for {}", request); - - org.eclipse.jgit.lib.Repository repo = open(); - - ObjectId revId = getCommitOrDefault(repo, request.getRevision()); - getContent(repo, revId, request.getPath(), output); + try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) { + closableObjectLoaderContainer.objectLoader.copyTo(output); + } } - /** - * Method description - * - * - * - * @param repo - * @param revId - * @param path - * @param output - * - * - * @throws IOException - * @throws RepositoryException - */ - void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, - String path, OutputStream output) - throws IOException, RepositoryException - { - TreeWalk treeWalk = null; - RevWalk revWalk = null; + @Override + public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException { + logger.debug("try to read content for {}", request); + return new InputStreamWrapper(getLoader(request)); + } - try - { - treeWalk = new TreeWalk(repo); - treeWalk.setRecursive(Util.nonNull(path).contains("/")); - - if (logger.isDebugEnabled()) - { - logger.debug("load content for {} at {}", path, revId.name()); - } - - revWalk = new RevWalk(repo); - - RevCommit entry = revWalk.parseCommit(revId); - RevTree revTree = entry.getTree(); - - if (revTree != null) - { - treeWalk.addTree(revTree); - } - else - { - logger.error("could not find tree for {}", revId.name()); - } - - treeWalk.setFilter(PathFilter.create(path)); - - if (treeWalk.next()) - { - - // Path exists - if (treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) - { - ObjectId blobId = treeWalk.getObjectId(0); - ObjectLoader loader = repo.open(blobId); - - loader.copyTo(output); - } - else - { - - // Not a blob, its something else (tree, gitlink) - throw new PathNotFoundException(path); - } - } - else - { - throw new PathNotFoundException(path); - } + void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException, RepositoryException { + try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) { + closableObjectLoaderContainer.objectLoader.copyTo(output); } - finally - { + } + + private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException, RepositoryException { + org.eclipse.jgit.lib.Repository repo = open(); + ObjectId revId = getCommitOrDefault(repo, request.getRevision()); + return getLoader(repo, revId, request.getPath()); + } + + private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException, RepositoryException { + TreeWalk treeWalk = new TreeWalk(repo); + treeWalk.setRecursive(Util.nonNull(path).contains("/")); + + logger.debug("load content for {} at {}", path, revId.name()); + + RevWalk revWalk = new RevWalk(repo); + + RevCommit entry = null; + try { + entry = revWalk.parseCommit(revId); + } catch (MissingObjectException e) { + throw new RevisionNotFoundException(revId.getName()); + } + RevTree revTree = entry.getTree(); + + if (revTree != null) { + treeWalk.addTree(revTree); + } else { + logger.error("could not find tree for {}", revId.name()); + } + + treeWalk.setFilter(PathFilter.create(path)); + + if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + ObjectId blobId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(blobId); + + return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk); + } else { + throw new PathNotFoundException(path); + } + } + + private static class ClosableObjectLoaderContainer implements Closeable { + private final ObjectLoader objectLoader; + private final TreeWalk treeWalk; + private final RevWalk revWalk; + + private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { + this.objectLoader = objectLoader; + this.treeWalk = treeWalk; + this.revWalk = revWalk; + } + + @Override + public void close() { GitUtil.release(revWalk); GitUtil.release(treeWalk); } } + + private static class InputStreamWrapper extends FilterInputStream { + + private final ClosableObjectLoaderContainer container; + + private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException { + super(container.objectLoader.openStream()); + this.container = container; + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + container.close(); + } + } + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index ede6a53429..c23db0873b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -32,19 +32,17 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.junit.Test; - +import sonia.scm.repository.GitConstants; +import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.RevisionNotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; -import sonia.scm.repository.GitConstants; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; /** * Unit tests for {@link GitCatCommand}. @@ -53,15 +51,8 @@ import sonia.scm.repository.GitConstants; * * @author Sebastian Sdorra */ -public class GitCatCommandTest extends AbstractGitCommandTestBase -{ +public class GitCatCommandTest extends AbstractGitCommandTestBase { - /** - * Tests cat command with default branch. - * - * @throws IOException - * @throws RepositoryException - */ @Test public void testDefaultBranch() throws IOException, RepositoryException { // without default branch, the repository head should be used @@ -75,16 +66,8 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase assertEquals("a and b", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -92,36 +75,46 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase assertEquals("a and b", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); assertEquals("b", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { + @Test(expected = PathNotFoundException.class) + public void testUnknownFile() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + execute(request); + } + + @Test(expected = RevisionNotFoundException.class) + public void testUnknownRevision() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + request.setPath("a.txt"); + execute(request); + } + + @Test + public void testSimpleStream() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("b.txt"); + + InputStream catResultStream = new GitCatCommand(createContext(), repository).getCatResultStream(request); + + assertEquals('b', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) throws IOException, RepositoryException { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java index 76438c1f1e..c0ae0b51b5 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java @@ -33,71 +33,44 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.aragost.javahg.commands.ExecutionException; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; - import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.web.HgUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -/** - * - * @author Sebastian Sdorra - */ -public class HgCatCommand extends AbstractCommand implements CatCommand -{ +public class HgCatCommand extends AbstractCommand implements CatCommand { - /** - * Constructs ... - * - * - * @param context - * @param repository - */ - HgCatCommand(HgCommandContext context, Repository repository) - { + HgCatCommand(HgCommandContext context, Repository repository) { super(context, repository); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param output - * - * @throws IOException - * @throws RepositoryException - */ @Override - public void getCatResult(CatCommandRequest request, OutputStream output) - throws IOException, RepositoryException - { + public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException { + InputStream input = getCatResultStream(request); + try { + ByteStreams.copy(input, output); + } finally { + Closeables.close(input, true); + } + } + + @Override + public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException { com.aragost.javahg.commands.CatCommand cmd = com.aragost.javahg.commands.CatCommand.on(open()); cmd.rev(HgUtil.getRevision(request.getRevision())); - InputStream input = null; - - try - { - input = cmd.execute(request.getPath()); - ByteStreams.copy(input, output); - } - finally - { - Closeables.close(input, true); + try { + return cmd.execute(request.getPath()); + } catch (ExecutionException e) { + throw new RepositoryException(e); } } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java index 49ef283c1a..afb5249b25 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java @@ -33,36 +33,19 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.junit.Test; - import sonia.scm.repository.RepositoryException; -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; -/** - * - * @author Sebastian Sdorra - */ -public class HgCatCommandTest extends AbstractHgCommandTestBase -{ +import static org.junit.Assert.assertEquals; + +public class HgCatCommandTest extends AbstractHgCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -70,48 +53,48 @@ public class HgCatCommandTest extends AbstractHgCommandTestBase assertEquals("a", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); assertEquals("b", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { - String content = null; + @Test(expected = RepositoryException.class) + public void testUnknownFile() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + execute(request); + } + + @Test(expected = RepositoryException.class) + public void testUnknownRevision() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setRevision("abc"); + request.setPath("a.txt"); + execute(request); + } + + @Test + public void testSimpleStream() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("b.txt"); + + InputStream catResultStream = new HgCatCommand(cmdContext, repository).getCatResultStream(request); + + assertEquals('b', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) throws IOException, RepositoryException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - try - { - new HgCatCommand(cmdContext, repository).getCatResult(request, baos); - } - finally - { - content = baos.toString().trim(); - } - - return content; + new HgCatCommand(cmdContext, repository).getCatResult(request, baos); + return baos.toString().trim(); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java index af08ea929e..4a8b907c29 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java @@ -37,22 +37,26 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNProperties; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.admin.SVNLookClient; - +import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.SvnUtil; -//~--- JDK imports ------------------------------------------------------------ - +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -120,6 +124,16 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } } + @Override + public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException { + // There seems to be no method creating an input stream as a result, so + // we have no other possibility then to copy the content into a buffer and + // stream it from there. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + getCatResult(request, output); + return new ByteArrayInputStream(output.toByteArray()); + } + /** * Method description * @@ -146,6 +160,17 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand } catch (SVNException ex) { + handleSvnException(request, ex); + } + } + + private void handleSvnException(CatCommandRequest request, SVNException ex) throws RepositoryException { + int svnErrorCode = ex.getErrorMessage().getErrorCode().getCode(); + if (SVNErrorCode.FS_NOT_FOUND.getCode() == svnErrorCode) { + throw new PathNotFoundException(request.getPath()); + } else if (SVNErrorCode.FS_NO_SUCH_REVISION.getCode() == svnErrorCode) { + throw new RevisionNotFoundException(request.getRevision()); + } else { throw new RepositoryException("could not get content from revision", ex); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java index 198a55edf3..f7e2123bad 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java @@ -32,36 +32,23 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.junit.Test; - +import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.RepositoryException; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.RevisionNotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; -/** - * - * @author Sebastian Sdorra - */ -public class SvnCatCommandTest extends AbstractSvnCommandTestBase -{ +import static org.junit.Assert.assertEquals; + +//~--- JDK imports ------------------------------------------------------------ + +public class SvnCatCommandTest extends AbstractSvnCommandTestBase { - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testCat() throws IOException, RepositoryException - { + public void testCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("a.txt"); @@ -69,36 +56,50 @@ public class SvnCatCommandTest extends AbstractSvnCommandTestBase assertEquals("a", execute(request)); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testSimpleCat() throws IOException, RepositoryException - { + public void testSimpleCat() throws IOException, RepositoryException { CatCommandRequest request = new CatCommandRequest(); request.setPath("c/d.txt"); assertEquals("d", execute(request)); } - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - * @throws RepositoryException - */ - private String execute(CatCommandRequest request) - throws IOException, RepositoryException - { + @Test(expected = PathNotFoundException.class) + public void testUnknownFile() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("unknown"); + request.setRevision("1"); + + execute(request); + } + + @Test(expected = RevisionNotFoundException.class) + public void testUnknownRevision() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + + request.setPath("a.txt"); + request.setRevision("42"); + + execute(request); + } + + @Test + public void testSimpleStream() throws IOException, RepositoryException { + CatCommandRequest request = new CatCommandRequest(); + request.setPath("a.txt"); + request.setRevision("1"); + + InputStream catResultStream = new SvnCatCommand(createContext(), repository).getCatResultStream(request); + + assertEquals('a', catResultStream.read()); + assertEquals('\n', catResultStream.read()); + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + + private String execute(CatCommandRequest request) throws IOException, RepositoryException { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index c622042061..0b0ad145c6 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -226,7 +226,18 @@ compiler ${mustache.version} - + + + com.github.sdorra + spotter-core + 1.1.0 + + + + org.apache.tika + tika-core + 1.18 + diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java index 234aae4015..73f59f14da 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java @@ -35,26 +35,20 @@ package sonia.scm.api.rest.resources; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.io.Closeables; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; -import java.io.OutputStream; +import sonia.scm.util.IOUtil; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; -import sonia.scm.util.IOUtil; +import java.io.IOException; +import java.io.OutputStream; /** * diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java new file mode 100644 index 0000000000..0936a5b4a0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -0,0 +1,154 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.spotter.ContentType; +import com.github.sdorra.spotter.ContentTypes; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.PathNotFoundException; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class ContentResource { + + private static final Logger LOG = LoggerFactory.getLogger(ContentResource.class); + private static final int HEAD_BUFFER_SIZE = 1024; + + private final RepositoryServiceFactory serviceFactory; + + @Inject + public ContentResource(RepositoryServiceFactory serviceFactory) { + this.serviceFactory = serviceFactory; + } + + /** + * Returns the content of a file for the given revision in the repository. The content type depends on the file + * content and can be discovered calling HEAD on the same URL. If a programming languge could be + * recognized, this will be given in the header Language. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * @param revision the revision + * @param path The path of the file + * + */ + @GET + @Path("{revision}/{path: .*}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + StreamingOutput stream = createStreamingOutput(namespace, name, revision, path, repositoryService); + Response.ResponseBuilder responseBuilder = Response.ok(stream); + return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder); + } catch (RepositoryNotFoundException e) { + LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + return Response.status(Status.NOT_FOUND).build(); + } + } + + private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, RepositoryService repositoryService) { + return os -> { + try { + repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path); + os.close(); + } catch (PathNotFoundException e) { + LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + throw new WebApplicationException(Status.NOT_FOUND); + } catch (RepositoryException e) { + LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e); + throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); + } + }; + } + + /** + * Returns the content type and the programming language (if it can be detected) of a file for the given revision in + * the repository. The programming language will be given in the header Language. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * @param revision the revision + * @param path The path of the file + * + */ + @HEAD + @Path("{revision}/{path: .*}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response metadata(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Response.ResponseBuilder responseBuilder = Response.ok(); + return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder); + } catch (RepositoryNotFoundException e) { + LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + return Response.status(Status.NOT_FOUND).build(); + } + } + + private Response createContentHeader(String namespace, String name, String revision, String path, RepositoryService repositoryService, Response.ResponseBuilder responseBuilder) { + try { + appendContentHeader(path, getHead(revision, path, repositoryService), responseBuilder); + } catch (PathNotFoundException e) { + LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); + return Response.status(Status.NOT_FOUND).build(); + } catch (RevisionNotFoundException e) { + LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e); + return Response.status(Status.NOT_FOUND).build(); + } catch (IOException | RepositoryException e) { + LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); + } + return responseBuilder.build(); + } + + private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) { + ContentType contentType = ContentTypes.detect(path, head); + responseBuilder.header("Content-Type", contentType.getRaw()); + contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language)); + } + + private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, RepositoryException { + InputStream stream = repositoryService.getCatCommand().setRevision(revision).getStream(path); + try { + byte[] buffer = new byte[HEAD_BUFFER_SIZE]; + int length = stream.read(buffer); + if (length < buffer.length) { + return Arrays.copyOf(buffer, length); + } else { + return buffer; + } + } finally { + IOUtil.close(stream); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 43aa6de608..817eb29f11 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -35,6 +35,7 @@ public class RepositoryResource { private final Provider branchRootResource; private final Provider changesetRootResource; private final Provider sourceRootResource; + private final Provider contentResource; private final Provider permissionRootResource; @Inject @@ -44,7 +45,7 @@ public class RepositoryResource { Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, - Provider sourceRootResource, Provider permissionRootResource) { + Provider sourceRootResource, Provider contentResource, Provider permissionRootResource) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -53,6 +54,7 @@ public class RepositoryResource { this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; this.sourceRootResource = sourceRootResource; + this.contentResource = contentResource; this.permissionRootResource = permissionRootResource; } @@ -151,6 +153,11 @@ public class RepositoryResource { return sourceRootResource.get(); } + @Path("content/") + public ContentResource content() { + return contentResource.get(); + } + @Path("permissions/") public PermissionRootResource permissions() { return permissionRootResource.get(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 711a5cf196..1b23b18f27 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -45,7 +45,7 @@ public class BranchRootResourceTest { public void prepareEnvironment() throws Exception { BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); BranchRootResource branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null)), null); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java new file mode 100644 index 0000000000..9302a4fcfd --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -0,0 +1,186 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.Test; +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.repository.NamespaceAndName; +import sonia.scm.repository.PathNotFoundException; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.CatCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ContentResourceTest { + + private static final String NAMESPACE = "space"; + private static final String REPO_NAME = "name"; + private static final String REV = "rev"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private RepositoryServiceFactory repositoryServiceFactory; + + @InjectMocks + private ContentResource contentResource; + + private CatCommandBuilder catCommand; + + @Before + public void initService() throws Exception { + NamespaceAndName existingNamespaceAndName = new NamespaceAndName(NAMESPACE, REPO_NAME); + RepositoryService repositoryService = repositoryServiceFactory.create(existingNamespaceAndName); + catCommand = repositoryService.getCatCommand(); + when(catCommand.setRevision(REV)).thenReturn(catCommand); + + // defaults for unknown things + doThrow(new RepositoryNotFoundException("x")).when(repositoryServiceFactory).create(not(eq(existingNamespaceAndName))); + doThrow(new PathNotFoundException("x")).when(catCommand).getStream(any()); + } + + @Test + public void shouldReadSimpleFile() throws Exception { + mockContent("file", "Hello".getBytes()); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file"); + assertEquals(200, response.getStatus()); + + ByteArrayOutputStream baos = readOutputStream(response); + + assertEquals("Hello", baos.toString()); + } + + @Test + public void shouldHandleMissingFile() { + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist"); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldHandleMissingRepository() { + Response response = contentResource.get("no", "repo", REV, "anything"); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldRecognizeTikaSourceCode() throws Exception { + mockContentFromResource("SomeGoCode.go"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go"); + assertEquals(200, response.getStatus()); + + assertEquals("GO", response.getHeaderString("Language")); + assertEquals("text/x-go", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldRecognizeSpecialSourceCode() throws Exception { + mockContentFromResource("Dockerfile"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile"); + assertEquals(200, response.getStatus()); + + assertEquals("DOCKERFILE", response.getHeaderString("Language")); + assertEquals("text/plain", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldRecognizeShebangSourceCode() throws Exception { + mockContentFromResource("someScript.sh"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "someScript.sh"); + assertEquals(200, response.getStatus()); + + assertEquals("PYTHON", response.getHeaderString("Language")); + assertEquals("application/x-sh", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldHandleRandomByteFile() throws Exception { + mockContentFromResource("JustBytes"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes"); + assertEquals(200, response.getStatus()); + + assertFalse(response.getHeaders().containsKey("Language")); + assertEquals("application/octet-stream", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldNotReadCompleteFileForHead() throws Exception { + FailingAfterSomeBytesStream stream = new FailingAfterSomeBytesStream(); + doAnswer(invocation -> stream).when(catCommand).getStream(eq("readHeadOnly")); + + Response response = contentResource.metadata(NAMESPACE, REPO_NAME, REV, "readHeadOnly"); + assertEquals(200, response.getStatus()); + + assertEquals("application/octet-stream", response.getHeaderString("Content-Type")); + assertTrue("stream has to be closed after reading head", stream.isClosed()); + } + + private void mockContentFromResource(String fileName) throws Exception { + URL url = Resources.getResource(fileName); + mockContent(fileName, Resources.toByteArray(url)); + } + + private void mockContent(String path, byte[] content) throws Exception { + doAnswer(invocation -> { + OutputStream outputStream = (OutputStream) invocation.getArguments()[0]; + outputStream.write(content); + outputStream.close(); + return null; + }).when(catCommand).retriveContent(any(), eq(path)); + doAnswer(invocation -> new ByteArrayInputStream(content)).when(catCommand).getStream(eq(path)); + } + + private ByteArrayOutputStream readOutputStream(Response response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ((StreamingOutput) response.getEntity()).write(baos); + return baos; + } + + private static class FailingAfterSomeBytesStream extends InputStream { + private int bytesRead = 0; + private boolean closed = false; + @Override + public int read() { + if (++bytesRead > 1024) { + fail("read too many bytes"); + } + return 0; + } + + @Override + public void close() throws IOException { + closed = true; + } + + public boolean isClosed() { + return closed; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 5de6d916ab..23cd51c9f9 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -53,11 +53,9 @@ import static org.mockito.MockitoAnnotations.initMocks; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j public class PermissionRootResourceTest { - public static final String REPOSITORY_NAMESPACE = "repo_namespace"; - public static final String REPOSITORY_NAME = "repo"; + private static final String REPOSITORY_NAMESPACE = "repo_namespace"; + private static final String REPOSITORY_NAME = "repo"; private static final String PERMISSION_NAME = "perm"; - - private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME; private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }"; @@ -93,7 +91,6 @@ public class PermissionRootResourceTest { .content(PERMISSION_TEST_PAYLOAD) .path(PATH_OF_ONE_PERMISSION); - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); @Rule @@ -102,7 +99,6 @@ public class PermissionRootResourceTest { @Mock private RepositoryManager repositoryManager; - private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -112,17 +108,15 @@ public class PermissionRootResourceTest { @InjectMocks private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; - private PermissionRootResource permissionRootResource; - @BeforeEach @Before public void prepareEnvironment() { initMocks(this); permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); @@ -130,32 +124,50 @@ public class PermissionRootResourceTest { dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); } + @TestFactory + @DisplayName("test endpoints on missing repository and user is Admin") + Stream missedRepositoryTestFactory() { + return createDynamicTestsToAssertResponses( + requestGETAllPermissions.expectedResponseStatus(404), + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(404), + requestDELETEPermission.expectedResponseStatus(404), + requestPUTPermission.expectedResponseStatus(404)); + } + + @TestFactory + @DisplayName("test endpoints on missing permission and user is Admin") + Stream missedPermissionTestFactory() { + authorizedUserHasARepository(); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(201), + requestGETAllPermissions.expectedResponseStatus(200), + requestDELETEPermission.expectedResponseStatus(204), + requestPUTPermission.expectedResponseStatus(404)); + } + + @TestFactory + @DisplayName("test endpoints on missing permission and user is not Admin") + Stream missedPermissionUserForbiddenTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(401), + requestPOSTPermission.expectedResponseStatus(401), + requestGETAllPermissions.expectedResponseStatus(401), + requestDELETEPermission.expectedResponseStatus(401), + requestPUTPermission.expectedResponseStatus(401)); + } @Test public void shouldGetAllPermissions() { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); - assertExpectedRequest(requestGETAllPermissions - .expectedResponseStatus(200) - .responseValidator((response) -> { - String body = response.getContentAsString(); - ObjectMapper mapper = new ObjectMapper(); - try { - List actualPermissionDtos = mapper.readValue(body, new TypeReference>() { - }); - assertThat(actualPermissionDtos) - .as("response payload match permission object models") - .hasSize(TEST_PERMISSIONS.size()) - .usingRecursiveFieldByFieldElementComparator() - .containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS)) - ; - } catch (IOException e) { - fail(); - } - }) - ); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS)); } - @Test public void shouldGetPermissionByName() { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); @@ -179,7 +191,6 @@ public class PermissionRootResourceTest { ); } - @Test public void shouldGetCreatedPermissions() { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); @@ -207,7 +218,6 @@ public class PermissionRootResourceTest { ); } - @Test public void shouldGetUpdatedPermissions() { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); @@ -287,7 +297,6 @@ public class PermissionRootResourceTest { ); } - private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions) { return permissions .stream() @@ -309,30 +318,6 @@ public class PermissionRootResourceTest { return result; } - @TestFactory - @DisplayName("test endpoints on missing repository and user is Admin") - Stream missedRepositoryTestFactory() { - return createDynamicTestsToAssertResponses( - requestGETAllPermissions.expectedResponseStatus(404), - requestGETPermission.expectedResponseStatus(404), - requestPOSTPermission.expectedResponseStatus(404), - requestDELETEPermission.expectedResponseStatus(404), - requestPUTPermission.expectedResponseStatus(404)); - } - - - @TestFactory - @DisplayName("test endpoints on missing permission and user is Admin") - Stream missedPermissionTestFactory() { - authorizedUserHasARepository(); - return createDynamicTestsToAssertResponses( - requestGETPermission.expectedResponseStatus(404), - requestPOSTPermission.expectedResponseStatus(201), - requestGETAllPermissions.expectedResponseStatus(200), - requestDELETEPermission.expectedResponseStatus(204), - requestPUTPermission.expectedResponseStatus(404)); - } - private Repository authorizedUserHasARepository() { Repository mockRepository = mock(Repository.class); when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); @@ -347,25 +332,7 @@ public class PermissionRootResourceTest { when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions); } - - @TestFactory - @DisplayName("test endpoints on missing permission and user is not Admin") - Stream missedPermissionUserForbiddenTestFactory() { - Repository mockRepository = mock(Repository.class); - when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); - when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); - return createDynamicTestsToAssertResponses( - requestGETPermission.expectedResponseStatus(401), - requestPOSTPermission.expectedResponseStatus(401), - requestGETAllPermissions.expectedResponseStatus(401), - requestDELETEPermission.expectedResponseStatus(401), - requestPUTPermission.expectedResponseStatus(401)); - } - - private Stream createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { - return Stream.of(expectedRequests) .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 75d09a2414..d47b26e5eb 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -74,7 +74,7 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); diff --git a/scm-webapp/src/test/resources/Dockerfile b/scm-webapp/src/test/resources/Dockerfile new file mode 100644 index 0000000000..d4b48dfd45 --- /dev/null +++ b/scm-webapp/src/test/resources/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu + +COPY nothing /nowhere + +RUN apt-get update + +EXEC shutdhown -h now + diff --git a/scm-webapp/src/test/resources/JustBytes b/scm-webapp/src/test/resources/JustBytes new file mode 100644 index 0000000000..f455a92b47 Binary files /dev/null and b/scm-webapp/src/test/resources/JustBytes differ diff --git a/scm-webapp/src/test/resources/SomeGoCode.go b/scm-webapp/src/test/resources/SomeGoCode.go new file mode 100644 index 0000000000..18d6395a74 --- /dev/null +++ b/scm-webapp/src/test/resources/SomeGoCode.go @@ -0,0 +1 @@ +package resources diff --git a/scm-webapp/src/test/resources/someScript.sh b/scm-webapp/src/test/resources/someScript.sh new file mode 100644 index 0000000000..0bcecb189d --- /dev/null +++ b/scm-webapp/src/test/resources/someScript.sh @@ -0,0 +1,6 @@ +#!/usr/bin/python + +for f in * ; do + ls $f +done +