diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index afb1417670..4cbf815e45 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -26,6 +26,7 @@ public class VndMediaType { public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX; + public static final String SOURCE = PREFIX + "source" + SUFFIX; private VndMediaType() { } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 861c9e7aee..a7ee8da41d 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -15,6 +15,8 @@ import java.io.File; import java.io.IOException; import java.util.Collection; +import static java.lang.Thread.sleep; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertNotNull; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @@ -69,4 +71,57 @@ public class RepositoryAccessITCase { assertNotNull(branchName); } + + @Test + public void shouldReadContent() throws IOException, InterruptedException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + tempFolder.newFolder("subfolder"); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "subfolder/a.txt", "sub-a"); + + sleep(1000); + + String sourcesUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.sources.href"); + + String rootContentUrl = given() + .when() + .get(sourcesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("files.find{it.name=='a.txt'}._links.self.href"); + given() + .when() + .get(rootContentUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("a")); + + String subfolderSourceUrl = given() + .when() + .get(sourcesUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("files.find{it.name=='subfolder'}._links.self.href"); + String subfolderContentUrl= given() + .when() + .get(subfolderSourceUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("files[0]._links.self.href"); + given() + .when() + .get(subfolderContentUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("sub-a")); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index e755f3c3c1..ef2332074b 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -37,11 +37,26 @@ public class RepositoryUtil { } static void createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { - Files.write(content, new File(repositoryClient.getWorkingCopy(), fileName), Charsets.UTF_8); - repositoryClient.getAddCommand().add(fileName); + File file = new File(repositoryClient.getWorkingCopy(), fileName); + Files.write(content, file, Charsets.UTF_8); + addWithParentDirectories(repositoryClient, file); commit(repositoryClient, username, "added " + fileName); } + private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { + File parent = file.getParentFile(); + String thisName = file.getName(); + String path; + if (!repositoryClient.getWorkingCopy().equals(parent)) { + addWithParentDirectories(repositoryClient, parent); + path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; + } else { + path = thisName; + } + repositoryClient.getAddCommand().add(path); + return path; + } + static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException { Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message); if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index 4dc0b8a33e..4e4721ba14 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -66,9 +66,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand //~--- get methods ---------------------------------------------------------- @Override - public BrowserResult getBrowserResult(BrowseCommandRequest request) - throws IOException - { + public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { HgFileviewCommand cmd = HgFileviewCommand.on(open()); if (!Strings.isNullOrEmpty(request.getRevision())) @@ -100,6 +98,12 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand result.setFiles(cmd.execute()); + if (!Strings.isNullOrEmpty(request.getRevision())) { + result.setRevision(request.getRevision()); + } else { + result.setRevision("tip"); + } + return result; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini similarity index 100% rename from scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini rename to scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java index 28419391ab..eb613e6144 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java @@ -347,6 +347,7 @@ public final class SvnUtil } public static long getRevisionNumber(String revision) throws RevisionNotFoundException { + // REVIEW Bei SVN wird ohne Revision die -1 genommen, was zu einem Fehler führt long revisionNumber = -1; if (Util.isNotEmpty(revision)) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 54b8458715..a75adf6b78 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -112,6 +112,10 @@ public class SvnBrowseCommand extends AbstractSvnCommand } } + if (revisionNumber == -1) { + revisionNumber = svnRepository.getLatestRevision(); + } + result = new BrowserResult(); result.setRevision(String.valueOf(revisionNumber)); result.setFiles(children); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 8d616ffb77..7ab3ef25a8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -29,7 +29,7 @@ public abstract class BranchToBranchDtoMapper { .self(resourceLinks.branch().self(namespaceAndName, target.getName())) .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()) - .single(linkBuilder("source", resourceLinks.source().source(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); + .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); target.add(linksBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultDto.java new file mode 100644 index 0000000000..b8ffd7ff26 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultDto.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Iterator; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class BrowserResultDto extends HalRepresentation implements Iterable { + private String revision; + private String tag; + private String branch; + // REVIEW files nicht embedded? + private List files; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + + // REVIEW return null? + @Override + public Iterator iterator() { + Iterator it = null; + + if (files != null) + { + it = files.iterator(); + } + + return it; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToBrowserResultDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToBrowserResultDtoMapper.java new file mode 100644 index 0000000000..7abb1ae69b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToBrowserResultDtoMapper.java @@ -0,0 +1,50 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import sonia.scm.repository.BrowserResult; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.NamespaceAndName; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +public class BrowserResultToBrowserResultDtoMapper { + + @Inject + private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper; + + @Inject + private ResourceLinks resourceLinks; + + public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) { + BrowserResultDto browserResultDto = new BrowserResultDto(); + + browserResultDto.setTag(browserResult.getTag()); + browserResultDto.setBranch(browserResult.getBranch()); + browserResultDto.setRevision(browserResult.getRevision()); + + List fileObjectDtoList = new ArrayList<>(); + for (FileObject fileObject : browserResult.getFiles()) { + fileObjectDtoList.add(mapFileObject(fileObject, namespaceAndName, browserResult.getRevision())); + } + + browserResultDto.setFiles(fileObjectDtoList); + this.addLinks(browserResult, browserResultDto, namespaceAndName); + return browserResultDto; + } + + private FileObjectDto mapFileObject(FileObject fileObject, NamespaceAndName namespaceAndName, String revision) { + return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision); + } + + private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName) { + if (browserResult.getRevision() == null) { + dto.add(Links.linkingTo().self(resourceLinks.source().selfWithoutRevision(namespaceAndName.getNamespace(), namespaceAndName.getName())).build()); + } else { + dto.add(Links.linkingTo().self(resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision())).build()); + } + } + + +} 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 index bf97a998d7..dc7c305823 100644 --- 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 @@ -60,8 +60,8 @@ public class ContentResource { @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) { + StreamingOutput stream = createStreamingOutput(namespace, name, revision, 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) { @@ -70,11 +70,14 @@ public class ContentResource { } } - private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, RepositoryService repositoryService) { + private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { return os -> { - try { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path); os.close(); + } catch (RepositoryNotFoundException e) { + LOG.debug("repository {}/{} not found", path, namespace, name, e); + throw new WebApplicationException(Status.NOT_FOUND); } catch (PathNotFoundException e) { LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e); throw new WebApplicationException(Status.NOT_FOUND); @@ -141,7 +144,9 @@ public class ContentResource { try { byte[] buffer = new byte[HEAD_BUFFER_SIZE]; int length = stream.read(buffer); - if (length < buffer.length) { + if (length < 0) { // empty file + return new byte[]{}; + } else if (length < buffer.length) { return Arrays.copyOf(buffer, length); } else { return buffer; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java new file mode 100644 index 0000000000..ab4986554a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -0,0 +1,28 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Instant; + +@Getter +@Setter +@NoArgsConstructor +public class FileObjectDto extends HalRepresentation { + private String name; + private String path; + private boolean directory; + private String description; + private int length; + private Instant lastModified; + private SubRepositoryDto subRepository; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java new file mode 100644 index 0000000000..bc814c7e0c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -0,0 +1,46 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.SubRepository; + +import javax.inject.Inject; +import java.net.URI; + +@Mapper +public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { + + @Inject + private ResourceLinks resourceLinks; + + protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision); + + abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); + + @AfterMapping + void addLinks(FileObject fileObject, @MappingTarget FileObjectDto dto, @Context NamespaceAndName namespaceAndName, @Context String revision) { + String path = removeFirstSlash(fileObject.getPath()); + Links.Builder links = Links.linkingTo(); + if (dto.isDirectory()) { + links.self(addPath(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path)); + } else { + links.self(addPath(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path)); + } + + dto.add(links.build()); + } + + // we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F' + private String addPath(String sourceWithPath, String path) { + return URI.create(sourceWithPath).resolve(path).toASCIIString(); + } + + private String removeFirstSlash(String source) { + return source.startsWith("/") ? source.substring(1) : source; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0605d943e7..9ceec9e230 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -28,6 +28,8 @@ public class MapperModule extends AbstractModule { bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); + bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); + bind(UriInfoStore.class).in(ServletScopes.REQUEST); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 0263b81048..e134ac8f7a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -47,7 +47,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper createFileObjects() { + List fileObjects = new ArrayList<>(); + + fileObjects.add(fileObject1); + fileObjects.add(fileObject2); + return fileObjects; + } + + private void assertEqualAttributes(BrowserResult browserResult, BrowserResultDto dto) { + assertThat(dto.getTag()).isEqualTo(browserResult.getTag()); + assertThat(dto.getBranch()).isEqualTo(browserResult.getBranch()); + assertThat(dto.getRevision()).isEqualTo(browserResult.getRevision()); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index ff97ca332b..033824cbea 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; 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; @@ -21,7 +22,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -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.MockitoAnnotations.initMocks; @SubjectAware( @@ -47,6 +50,13 @@ public class ConfigResourceTest { @InjectMocks private ScmConfigurationToConfigDtoMapperImpl configToDtoMapper; + public ConfigResourceTest() { + // cleanup state that might have been left by other tests + ThreadContext.unbindSecurityManager(); + ThreadContext.unbindSubject(); + ThreadContext.remove(); + } + @Before public void prepareEnvironment() { initMocks(this); 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 index 9302a4fcfd..3d898119fb 100644 --- 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 @@ -108,17 +108,6 @@ public class ContentResourceTest { 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"); @@ -142,6 +131,17 @@ public class ContentResourceTest { assertTrue("stream has to be closed after reading head", stream.isClosed()); } + @Test + public void shouldHandleEmptyFile() throws Exception { + mockContent("empty", new byte[]{}); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "empty"); + assertEquals(200, response.getStatus()); + + assertFalse(response.getHeaders().containsKey("Language")); + assertEquals("application/octet-stream", response.getHeaderString("Content-Type")); + } + private void mockContentFromResource(String fileName) throws Exception { URL url = Resources.getResource(fileName); mockContent(fileName, Resources.toByteArray(url)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java new file mode 100644 index 0000000000..23b723b748 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapperTest.java @@ -0,0 +1,102 @@ +package sonia.scm.api.v2.resources; + +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.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.SubRepository; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class FileObjectToFileObjectDtoMapperTest { + + private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private FileObjectToFileObjectDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + expectedBaseUri = baseUri.resolve(RepositoryRootResource.REPOSITORIES_PATH_V2 + "/"); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbind() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapAttributesCorrectly() { + FileObject fileObject = createFileObject(); + FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), "revision"); + + assertEqualAttributes(fileObject, dto); + } + + @Test + public void shouldHaveCorrectSelfLinkForDirectory() { + FileObject fileObject = createDirectoryObject(); + FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), "revision"); + + assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo(expectedBaseUri.resolve("namespace/name/sources/revision/foo/bar").toString()); + } + + @Test + public void shouldHaveCorrectContentLink() { + FileObject fileObject = createFileObject(); + fileObject.setDirectory(false); + FileObjectDto dto = mapper.map(fileObject, new NamespaceAndName("namespace", "name"), "revision"); + + assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo(expectedBaseUri.resolve("namespace/name/content/revision/foo/bar").toString()); + } + + private FileObject createDirectoryObject() { + FileObject fileObject = createFileObject(); + fileObject.setDirectory(true); + return fileObject; + } + + private FileObject createFileObject() { + FileObject fileObject = new FileObject(); + fileObject.setName("foo"); + fileObject.setDescription("bar"); + fileObject.setPath("foo/bar"); + fileObject.setDirectory(false); + fileObject.setLength(100); + fileObject.setLastModified(123L); + + fileObject.setSubRepository(new SubRepository("repo.url")); + return fileObject; + } + + private void assertEqualAttributes(FileObject fileObject, FileObjectDto dto) { + assertThat(dto.getName()).isEqualTo(fileObject.getName()); + assertThat(dto.getDescription()).isEqualTo(fileObject.getDescription()); + assertThat(dto.getPath()).isEqualTo(fileObject.getPath()); + assertThat(dto.isDirectory()).isEqualTo(fileObject.isDirectory()); + assertThat(dto.getLength()).isEqualTo(fileObject.getLength()); + assertThat(dto.getLastModified().toEpochMilli()).isEqualTo((long) fileObject.getLastModified()); + assertThat(dto.getSubRepository().getBrowserUrl()).isEqualTo(fileObject.getSubRepository().getBrowserUrl()); + } +} 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 588ec51134..227775cf16 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 @@ -17,6 +17,7 @@ import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.spi.HttpRequest; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -141,6 +142,11 @@ public class PermissionRootResourceTest { ThreadContext.bind(subject); } + @After + public void unbind() { + ThreadContext.unbindSubject(); + } + @TestFactory @DisplayName("test endpoints on missing repository") Stream missedRepositoryTestFactory() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 000e628f26..33607c0a6d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -135,8 +135,8 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectBranchSourceUrl() { - String url = resourceLinks.source().source("space", "name", "revision"); - assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/sources/revision", url); + String url = resourceLinks.source().selfWithoutRevision("space", "name"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/sources/", url); } @Test @@ -147,13 +147,18 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectSourceCollectionUrl() { - String url = resourceLinks.source().self("space", "repo"); + String url = resourceLinks.source().selfWithoutRevision("space", "repo"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/", url); } + @Test + public void shouldCreateCorrectSourceUrlWithFilename() { + String url = resourceLinks.source().sourceWithPath("foo", "bar", "rev", "file"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "foo/bar/sources/rev/file", url); + } @Test public void shouldCreateCorrectPermissionCollectionUrl() { - String url = resourceLinks.source().self("space", "repo"); + String url = resourceLinks.source().selfWithoutRevision("space", "repo"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/", url); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java new file mode 100644 index 0000000000..1e73c6dfaa --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -0,0 +1,160 @@ +package sonia.scm.api.v2.resources; + +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.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.BrowserResult; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.BrowseCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.Silent.class) +public class SourceRootResourceTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private BrowseCommandBuilder browseCommandBuilder; + + @Mock + private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper; + + @InjectMocks + private BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper; + + + @Before + public void prepareEnvironment() throws Exception { + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(service.getBrowseCommand()).thenReturn(browseCommandBuilder); + + FileObjectDto dto = new FileObjectDto(); + dto.setName("name"); + dto.setLength(1024); + + when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto); + SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper); + RepositoryRootResource repositoryRootResource = + new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, + null, + null, + null, + null, + null, + MockProvider.of(sourceRootResource), + null, + null)), + null); + + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + } + + @Test + public void shouldReturnSources() throws URISyntaxException, IOException, RevisionNotFoundException { + BrowserResult result = createBrowserResult(); + when(browseCommandBuilder.getBrowserResult()).thenReturn(result); + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentAsString()).contains("\"revision\":\"revision\""); + assertThat(response.getContentAsString()).contains("\"tag\":\"tag\""); + assertThat(response.getContentAsString()).contains("\"branch\":\"branch\""); + assertThat(response.getContentAsString()).contains("\"files\":"); + } + + @Test + public void shouldReturn404IfRepoNotFound() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(RepositoryNotFoundException.class); + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "idont/exist/sources"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, RevisionNotFoundException { + BrowserResult browserResult = new BrowserResult(); + browserResult.setBranch("abc"); + browserResult.setRevision("revision"); + browserResult.setTag("tag"); + FileObject fileObject = new FileObject(); + fileObject.setName("File Object!"); + + browserResult.setFiles(Arrays.asList(fileObject)); + + when(browseCommandBuilder.getBrowserResult()).thenReturn(browserResult); + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/revision/fileabc"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentAsString()).contains("\"revision\":\"revision\""); + } + + @Test + public void shouldGet404ForSingleFileIfRepoNotFound() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(new NamespaceAndName("idont", "exist"))).thenThrow(RepositoryNotFoundException.class); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "idont/exist/sources/revision/fileabc"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + assertThat(response.getStatus()).isEqualTo(404); + } + + private BrowserResult createBrowserResult() { + return new BrowserResult("revision", "tag", "branch", createFileObjects()); + } + + private List createFileObjects() { + FileObject fileObject1 = new FileObject(); + fileObject1.setName("FO 1"); + fileObject1.setDirectory(false); + fileObject1.setDescription("File object 1"); + fileObject1.setPath("/foo/bar/fo1"); + fileObject1.setLength(1024L); + fileObject1.setLastModified(0L); + + FileObject fileObject2 = new FileObject(); + fileObject2.setName("FO 2"); + fileObject2.setDirectory(true); + fileObject2.setDescription("File object 2"); + fileObject2.setPath("/foo/bar/fo2"); + fileObject2.setLength(4096L); + fileObject2.setLastModified(1234L); + + return Arrays.asList(fileObject1, fileObject2); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index 72aa98091b..6dda005019 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -36,18 +36,26 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.Sets; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import org.apache.shiro.util.ThreadContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import static org.mockito.Mockito.*; -import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; /** * Unit test for {@link JwtAccessTokenBuilder}. @@ -57,6 +65,10 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class JwtAccessTokenBuilderTest { + { + ThreadContext.unbindSubject(); + } + @Mock private KeyGenerator keyGenerator;