Feature/branch details (#1876)

Enrich branch overview with more details like last committer and ahead/behind commits. Since calculating this information is pretty intense, we request it in chunks to prevent very long loading times. Also we cache the results in frontend and backend.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-12-01 14:19:18 +01:00
committed by GitHub
parent ce2eae1843
commit 9cc134f5a8
59 changed files with 1933 additions and 154 deletions

View File

@@ -0,0 +1,64 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
class BranchDetailsMapperTest {
private final Repository repository = RepositoryTestData.create42Puzzle();
BranchDetailsMapper mapper = new BranchDetailsMapperImpl();
@BeforeEach
void configureMapper() {
ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore();
scmPathInfoStore.set(() -> URI.create("/scm/api/"));
mapper.setResourceLinks(new ResourceLinks(scmPathInfoStore));
}
@Test
void shouldMapDto() {
BranchDetailsDto dto = mapper.map(
repository,
"master",
new BranchDetailsCommandResult(42, 21)
);
assertThat(dto.getBranchName()).isEqualTo("master");
assertThat(dto.getChangesetsAhead()).isEqualTo(42);
assertThat(dto.getChangesetsBehind()).isEqualTo(21);
assertThat(dto.getLinks().getLinkBy("self").get().getHref())
.isEqualTo("/scm/api/v2/repositories/hitchhiker/42Puzzle/branch-details/master");
}
}

View File

@@ -0,0 +1,185 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.api.BranchDetailsCommandBuilder;
import sonia.scm.repository.api.BranchDetailsCommandResult;
import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.JsonMockHttpResponse;
import sonia.scm.web.RestDispatcher;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BranchDetailsResourceTest extends RepositoryTestBase {
private final RestDispatcher dispatcher = new RestDispatcher();
private final Repository repository = RepositoryTestData.create42Puzzle();
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock
private RepositoryServiceFactory serviceFactory;
@Mock
private RepositoryService service;
@Mock
private BranchDetailsCommandBuilder branchDetailsCommandBuilder;
private final BranchDetailsMapperImpl mapper = new BranchDetailsMapperImpl();
private final JsonMockHttpResponse response = new JsonMockHttpResponse();
@BeforeEach
void prepareEnvironment() {
super.branchDetailsResource = new BranchDetailsResource(serviceFactory, mapper, resourceLinks);
dispatcher.addSingletonResource(getRepositoryRootResource());
ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore();
scmPathInfoStore.set(() -> URI.create("/scm/api/"));
mapper.setResourceLinks(new ResourceLinks(scmPathInfoStore));
}
@Test
void shouldReturnBadRequestIfBranchDetailsNotSupported() throws URISyntaxException {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
doThrow(CommandNotSupportedException.class).when(service).getBranchDetailsCommand();
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details/master/");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
void shouldReturnBranchDetails() throws URISyntaxException, UnsupportedEncodingException {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
when(service.getRepository()).thenReturn(repository);
when(service.getBranchDetailsCommand()).thenReturn(branchDetailsCommandBuilder);
BranchDetailsCommandResult result = new BranchDetailsCommandResult(42, 21);
when(branchDetailsCommandBuilder.execute("master")).thenReturn(result);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details/master/");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.isEqualTo("{\"branchName\":\"master\",\"changesetsAhead\":42,\"changesetsBehind\":21,\"_links\":{\"self\":{\"href\":\"/scm/api/v2/repositories/hitchhiker/42Puzzle/branch-details/master\"}}}");
}
@ParameterizedTest
@ValueSource(strings = {
"%2Fmaster",
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890X"
})
void shouldValidateSingleBranch(String branchName) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + String.format("/branch-details/%s/", branchName));
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
void shouldGetEmptyDetailsCollection() throws URISyntaxException, UnsupportedEncodingException {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details/");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).isEqualTo("{\"_links\":{\"self\":{\"href\":\"/v2/repositories/hitchhiker/42Puzzle/branch-details/\"}},\"_embedded\":{\"branchDetails\":[]}}");
}
@Test
void shouldGetDetailsCollection() throws URISyntaxException, UnsupportedEncodingException {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
when(service.getRepository()).thenReturn(repository);
when(service.getBranchDetailsCommand()).thenReturn(branchDetailsCommandBuilder);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details?branches=master&branches=develop&branches=feature%2Fhitchhiker42");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).contains("{\"branchDetails\":[{\"branchName\":\"master\",\"_links\":{\"self\":{\"href\":\"/scm/api/v2/repositories/hitchhiker/42Puzzle/branch-details/master\"}}}");
assertThat(response.getContentAsString()).contains("{\"branchName\":\"develop\",\"_links\":{\"self\":{\"href\":\"/scm/api/v2/repositories/hitchhiker/42Puzzle/branch-details/develop\"}}}");
assertThat(response.getContentAsString()).contains("{\"branchName\":\"feature/hitchhiker42\",\"_links\":{\"self\":{\"href\":\"/scm/api/v2/repositories/hitchhiker/42Puzzle/branch-details/feature%2Fhitchhiker42\"}}}");
}
@ParameterizedTest
@ValueSource(strings = {
"%2Fmaster",
"",
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890X"
})
void shouldRejectInvalidBranchInCollection(String branchName) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + String.format("/branch-details?branches=ok&branches=%s", branchName));
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
void shouldIgnoreMissingBranchesInCollection() throws URISyntaxException {
when(serviceFactory.create(repository.getNamespaceAndName())).thenReturn(service);
when(service.getBranchDetailsCommand()).thenReturn(branchDetailsCommandBuilder);
when(branchDetailsCommandBuilder.execute("no-such-branch")).thenThrow(NotFoundException.class);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + repository.getNamespaceAndName() + "/branch-details?branches=no-such-branch");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsJson().get("_embedded").get("branchDetails")).isEmpty();
}
}

View File

@@ -36,6 +36,7 @@ abstract class RepositoryTestBase {
RepositoryManager manager;
TagRootResource tagRootResource;
BranchRootResource branchRootResource;
BranchDetailsResource branchDetailsResource;
ChangesetRootResource changesetRootResource;
SourceRootResource sourceRootResource;
ContentResource contentResource;
@@ -55,6 +56,7 @@ abstract class RepositoryTestBase {
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
of(tagRootResource),
of(branchRootResource),
of(branchDetailsResource),
of(changesetRootResource),
of(sourceRootResource),
of(contentResource),

View File

@@ -336,6 +336,14 @@ public class RepositoryToRepositoryDtoMapperTest {
assertTrue(dto.isHealthCheckRunning());
}
@Test
public void shouldAppendBranchDetailsLinkIfSupported() {
Repository testRepository = createTestRepository();
when(repositoryService.isSupported(Command.BRANCH_DETAILS)).thenReturn(true);
RepositoryDto dto = mapper.map(testRepository);
assertTrue(dto.getLinks().getLinkBy("branchDetailsCollection").isPresent());
}
@Test
public void shouldCreateCorrectLinksForHealthChecks() {
when(scmContextProvider.getDocumentationVersion()).thenReturn("2.17.x");

View File

@@ -83,6 +83,8 @@ public class ResourceLinksMock {
lenient().when(resourceLinks.apiKeyCollection()).thenReturn(new ResourceLinks.ApiKeyCollectionLinks(pathInfo));
lenient().when(resourceLinks.apiKey()).thenReturn(new ResourceLinks.ApiKeyLinks(pathInfo));
lenient().when(resourceLinks.search()).thenReturn(new ResourceLinks.SearchLinks(pathInfo));
lenient().when(resourceLinks.branchDetails()).thenReturn(new ResourceLinks.BranchDetailsLinks(pathInfo));
lenient().when(resourceLinks.branchDetailsCollection()).thenReturn(new ResourceLinks.BranchDetailsCollectionLinks(pathInfo));
return resourceLinks;
}