Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-09-10 09:24:45 +02:00
24 changed files with 519 additions and 106 deletions

2
Jenkinsfile vendored
View File

@@ -74,7 +74,7 @@ Maven setupMavenBuild() {
void analyzeWith(Maven mvn) { void analyzeWith(Maven mvn) {
withSonarQubeEnv('sonarcloud.io') { withSonarQubeEnv('sonarcloud.io-scm') {
String mvnArgs = "${env.SONAR_MAVEN_GOAL} " + String mvnArgs = "${env.SONAR_MAVEN_GOAL} " +
"-Dsonar.host.url=${env.SONAR_HOST_URL} " + "-Dsonar.host.url=${env.SONAR_HOST_URL} " +

View File

@@ -19,6 +19,8 @@ public class VndMediaType {
public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;

View File

@@ -1,5 +1,7 @@
package sonia.scm.it; package sonia.scm.it;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
@@ -8,18 +10,22 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static java.lang.Thread.sleep; import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes; import static sonia.scm.it.ScmTypes.availableScmTypes;
@@ -74,6 +80,85 @@ public class RepositoryAccessITCase {
assertNotNull(branchName); assertNotNull(branchName);
} }
@Test
public void shouldFindTags() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
Assume.assumeTrue("There are no tags for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.TAG));
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "a.txt", "a");
String tagName = "v1.0";
String repositoryUrl = TestData.getDefaultRepositoryUrl(repositoryType);
String tagsUrl = given()
.when()
.get(repositoryUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.path("_links.tags.href");
ExtractableResponse<Response> response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD)
.when()
.get(tagsUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract();
assertThat(response).isNotNull();
assertThat(response.body()).isNotNull();
assertThat(response.body().asString())
.isNotNull()
.isNotBlank();
RepositoryUtil.addTag(repositoryClient, changeset.getId(), tagName);
response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD)
.when()
.get(tagsUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract();
assertThat(response).isNotNull();
assertThat(response.body()).isNotNull();
assertThat(response.body().asString())
.isNotNull()
.isNotBlank();
assertThat(response.body().jsonPath().getString("_links.self.href"))
.as("assert tags self link")
.isNotNull()
.contains(repositoryUrl + "/tags/");
assertThat(response.body().jsonPath().getList("_embedded.tags"))
.as("assert tag size")
.isNotNull()
.size()
.isGreaterThan(0);
assertThat(response.body().jsonPath().getMap("_embedded.tags.find{it.name=='" + tagName + "'}"))
.as("assert tag name and revision")
.isNotNull()
.hasSize(3)
.containsEntry("name", tagName)
.containsEntry("revision", changeset.getId());
assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.self.href"))
.as("assert single tag self link")
.isNotNull()
.contains(String.format("%s/tags/%s", repositoryUrl, tagName));
assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.sources.href"))
.as("assert single tag source link")
.isNotNull()
.contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId()));
assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changesets.href"))
.as("assert single tag changesets link")
.isNotNull()
.contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId()));
}
@Test @Test
public void shouldReadContent() throws IOException, InterruptedException { public void shouldReadContent() throws IOException, InterruptedException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
@@ -97,7 +182,8 @@ public class RepositoryAccessITCase {
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("files.find{it.name=='a.txt'}._links.self.href"); .path("_embedded.files.find{it.name=='a.txt'}._links.self.href");
given() given()
.when() .when()
.get(rootContentUrl) .get(rootContentUrl)
@@ -111,14 +197,22 @@ public class RepositoryAccessITCase {
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("files.find{it.name=='subfolder'}._links.self.href"); .path("_embedded.files.find{it.name=='subfolder'}._links.self.href");
String subfolderContentUrl= given() String selfOfSubfolderUrl = given()
.when() .when()
.get(subfolderSourceUrl) .get(subfolderSourceUrl)
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("files[0]._links.self.href"); .path("_links.self.href");
assertThat(subfolderSourceUrl).isEqualTo(selfOfSubfolderUrl);
String subfolderContentUrl = given()
.when()
.get(subfolderSourceUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.path("_embedded.files[0]._links.self.href");
given() given()
.when() .when()
.get(subfolderContentUrl) .get(subfolderContentUrl)

View File

@@ -7,6 +7,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.repository.Tag;
import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientFactory; import sonia.scm.repository.client.api.RepositoryClientFactory;
@@ -40,11 +41,11 @@ public class RepositoryUtil {
return name; return name;
} }
static void createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
File file = new File(repositoryClient.getWorkingCopy(), fileName); File file = new File(repositoryClient.getWorkingCopy(), fileName);
Files.write(content, file, Charsets.UTF_8); Files.write(content, file, Charsets.UTF_8);
addWithParentDirectories(repositoryClient, file); addWithParentDirectories(repositoryClient, file);
commit(repositoryClient, username, "added " + fileName); return commit(repositoryClient, username, "added " + fileName);
} }
private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException {
@@ -69,4 +70,16 @@ public class RepositoryUtil {
} }
return changeset; return changeset;
} }
static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
repositoryClient.getPushCommand().pushTags();
}
return tag;
}
return null;
}
} }

View File

@@ -37,12 +37,12 @@ package sonia.scm.repository.client.spi;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
import java.util.function.Supplier;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
@@ -73,11 +73,20 @@ public class GitPushCommand implements PushCommand
* @throws IOException * @throws IOException
*/ */
@Override @Override
public void push() throws IOException public void push() throws IOException {
push(() -> git.push().setPushAll());
}
@Override
public void pushTags() throws IOException {
push(() -> git.push().setPushTags());
}
private void push(Supplier<org.eclipse.jgit.api.PushCommand> commandSupplier) throws RepositoryClientException
{ {
try try
{ {
org.eclipse.jgit.api.PushCommand cmd = git.push().setPushAll(); org.eclipse.jgit.api.PushCommand cmd = commandSupplier.get();
if (credentialsProvider != null) if (credentialsProvider != null)
{ {

View File

@@ -35,22 +35,20 @@ package sonia.scm.repository.client.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings; import com.google.common.base.Strings;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -119,7 +117,11 @@ public class GitTagCommand implements TagCommand
ref = git.tag().setObjectId(revObject).call(); ref = git.tag().setObjectId(revObject).call();
} }
tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); if (ref.isPeeled()) {
tag = new Tag(request.getName(), ref.getPeeledObjectId().toString());
} else {
tag = new Tag(request.getName(), ref.getObjectId().toString());
}
} }
catch (GitAPIException ex) catch (GitAPIException ex)

View File

@@ -32,9 +32,10 @@ package sonia.scm.repository.client.spi;
import com.aragost.javahg.Repository; import com.aragost.javahg.Repository;
import com.aragost.javahg.commands.ExecutionException; import com.aragost.javahg.commands.ExecutionException;
import java.io.IOException;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
import java.io.IOException;
/** /**
* Mercurial implementation of the {@link PushCommand}. * Mercurial implementation of the {@link PushCommand}.
* *
@@ -63,5 +64,10 @@ public class HgPushCommand implements PushCommand
throw new RepositoryClientException("push to repository failed", ex); throw new RepositoryClientException("push to repository failed", ex);
} }
} }
@Override
public void pushTags() throws IOException {
push();
}
} }

View File

@@ -32,7 +32,6 @@ package sonia.scm.repository.client.spi;
import com.aragost.javahg.Repository; import com.aragost.javahg.Repository;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.io.IOException;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
/** /**
@@ -51,13 +50,16 @@ public class HgTagCommand implements TagCommand
} }
@Override @Override
public Tag tag(TagRequest request) throws IOException public Tag tag(TagRequest request)
{ {
String rev = request.getRevision(); String rev = request.getRevision();
if ( Strings.isNullOrEmpty(rev) ){ if ( Strings.isNullOrEmpty(rev) ){
rev = repository.tip().getNode(); rev = repository.tip().getNode();
} }
com.aragost.javahg.commands.TagCommand.on(repository).rev(rev).execute(request.getName()); com.aragost.javahg.commands.TagCommand.on(repository)
.rev(rev)
.user(request.getUserName())
.execute(request.getName());
return new Tag(request.getName(), rev); return new Tag(request.getName(), rev);
} }

View File

@@ -36,13 +36,12 @@ package sonia.scm.repository.client.api;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.client.spi.PushCommand; import sonia.scm.repository.client.spi.PushCommand;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -88,6 +87,14 @@ public final class PushCommandBuilder
command.push(); command.push();
} }
public void pushTags() throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("push tag changes back to main repository");
}
command.pushTags();
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */

View File

@@ -36,15 +36,14 @@ package sonia.scm.repository.client.api;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.Tag; import sonia.scm.repository.Tag;
import sonia.scm.repository.client.spi.TagCommand; import sonia.scm.repository.client.spi.TagCommand;
import sonia.scm.repository.client.spi.TagRequest; import sonia.scm.repository.client.spi.TagRequest;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -84,9 +83,10 @@ public final class TagCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public Tag tag(String name) throws IOException public Tag tag(String name, String username) throws IOException
{ {
request.setName(name); request.setName(name);
request.setUsername(username);
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {

View File

@@ -44,11 +44,7 @@ import java.io.IOException;
public interface PushCommand public interface PushCommand
{ {
/** void push() throws IOException;
* Method description
* void pushTags() throws IOException;
*
* @throws IOException
*/
public void push() throws IOException;
} }

View File

@@ -91,6 +91,7 @@ public final class TagRequest
{ {
this.name = null; this.name = null;
this.revision = null; this.revision = null;
this.username = null;
} }
/** /**
@@ -106,6 +107,7 @@ public final class TagRequest
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("revision", revision) .add("revision", revision)
.add("name", name) .add("name", name)
.add("username", username)
.toString(); .toString();
//J+ //J+
} }
@@ -134,6 +136,10 @@ public final class TagRequest
this.revision = revision; this.revision = revision;
} }
public void setUsername(String username) {
this.username = username;
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**
@@ -158,6 +164,10 @@ public final class TagRequest
return revision; return revision;
} }
public String getUserName() {
return username;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
@@ -165,4 +175,6 @@ public final class TagRequest
/** Field description */ /** Field description */
private String revision; private String revision;
private String username;
} }

View File

@@ -6,18 +6,13 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.util.Iterator;
import java.util.List; import java.util.List;
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
public class BrowserResultDto extends HalRepresentation implements Iterable<FileObjectDto> { public class BrowserResultDto extends HalRepresentation {
private String revision; private String revision;
private String tag;
private String branch;
// REVIEW files nicht embedded?
private List<FileObjectDto> files;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package
@@ -25,16 +20,7 @@ public class BrowserResultDto extends HalRepresentation implements Iterable<File
return super.add(links); return super.add(links);
} }
// REVIEW return null? public void setFiles(List<FileObjectDto> files) {
@Override this.withEmbedded("files", files);
public Iterator<FileObjectDto> iterator() {
Iterator<FileObjectDto> it = null;
if (files != null)
{
it = files.iterator();
}
return it;
} }
} }

View File

@@ -17,11 +17,9 @@ public class BrowserResultToBrowserResultDtoMapper {
@Inject @Inject
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) { public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, String path) {
BrowserResultDto browserResultDto = new BrowserResultDto(); BrowserResultDto browserResultDto = new BrowserResultDto();
browserResultDto.setTag(browserResult.getTag());
browserResultDto.setBranch(browserResult.getBranch());
browserResultDto.setRevision(browserResult.getRevision()); browserResultDto.setRevision(browserResult.getRevision());
List<FileObjectDto> fileObjectDtoList = new ArrayList<>(); List<FileObjectDto> fileObjectDtoList = new ArrayList<>();
@@ -30,7 +28,7 @@ public class BrowserResultToBrowserResultDtoMapper {
} }
browserResultDto.setFiles(fileObjectDtoList); browserResultDto.setFiles(fileObjectDtoList);
this.addLinks(browserResult, browserResultDto, namespaceAndName); this.addLinks(browserResult, browserResultDto, namespaceAndName, path);
return browserResultDto; return browserResultDto;
} }
@@ -38,13 +36,14 @@ public class BrowserResultToBrowserResultDtoMapper {
return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision); return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision);
} }
private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName) { private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName, String path) {
if (path.equals("/")) {
path = "";
}
if (browserResult.getRevision() == null) { if (browserResult.getRevision() == null) {
dto.add(Links.linkingTo().self(resourceLinks.source().selfWithoutRevision(namespaceAndName.getNamespace(), namespaceAndName.getName())).build()); throw new IllegalStateException("missing revision in browser result for repository " + namespaceAndName + " and path " + path);
} else { } else {
dto.add(Links.linkingTo().self(resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision())).build()); dto.add(Links.linkingTo().self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)).build());
} }
} }
} }

View File

@@ -10,7 +10,6 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.SubRepository; import sonia.scm.repository.SubRepository;
import javax.inject.Inject; import javax.inject.Inject;
import java.net.URI;
@Mapper @Mapper
public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> { public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> {
@@ -27,19 +26,14 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObj
String path = removeFirstSlash(fileObject.getPath()); String path = removeFirstSlash(fileObject.getPath());
Links.Builder links = Links.linkingTo(); Links.Builder links = Links.linkingTo();
if (dto.isDirectory()) { if (dto.isDirectory()) {
links.self(addPath(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path)); links.self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
} else { } else {
links.self(addPath(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, ""), path)); links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), revision, path));
} }
dto.add(links.build()); 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) { private String removeFirstSlash(String source) {
return source.startsWith("/") ? source.substring(1) : source; return source.startsWith("/") ? source.substring(1) : source;
} }

View File

@@ -213,8 +213,8 @@ class ResourceLinks {
tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class); tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class);
} }
String self(String namespace, String name, String id) { String self(String namespace, String name, String tagName) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(id).href(); return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(tagName).href();
} }
String all(String namespace, String name) { String all(String namespace, String name) {
@@ -326,15 +326,16 @@ class ResourceLinks {
} }
public String sourceWithPath(String namespace, String name, String revision, String path) { public String sourceWithPath(String namespace, String name, String revision, String path) {
if (revision == null) { return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision, "").href(), path);
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(null, path).href();
} else {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision, path).href();
}
} }
public String content(String namespace, String name, String revision, String path) { public String content(String namespace, String name, String revision, String path) {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, path).href(); return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path);
}
// 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();
} }
} }
public PermissionLinks permission() { public PermissionLinks permission() {

View File

@@ -62,7 +62,7 @@ public class SourceRootResource {
BrowserResult browserResult = browseCommand.getBrowserResult(); BrowserResult browserResult = browseCommand.getBrowserResult();
if (browserResult != null) { if (browserResult != null) {
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName)).build(); return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName, path)).build();
} else { } else {
return Response.status(Response.Status.NOT_FOUND).build(); return Response.status(Response.Status.NOT_FOUND).build();
} }

View File

@@ -0,0 +1,7 @@
package sonia.scm.api.v2.resources;
import sonia.scm.NotFoundException;
public class TagNotFoundException extends NotFoundException {
}

View File

@@ -1,27 +1,100 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import javax.ws.rs.DefaultValue; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.Tag;
import sonia.scm.repository.Tags;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.QueryParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException;
public class TagRootResource { public class TagRootResource {
private final RepositoryServiceFactory serviceFactory;
private final TagCollectionToDtoMapper tagCollectionToDtoMapper;
private final TagToTagDtoMapper tagToTagDtoMapper;
@Inject
public TagRootResource(RepositoryServiceFactory serviceFactory,
TagCollectionToDtoMapper tagCollectionToDtoMapper,
TagToTagDtoMapper tagToTagDtoMapper) {
this.serviceFactory = serviceFactory;
this.tagCollectionToDtoMapper = tagCollectionToDtoMapper;
this.tagToTagDtoMapper = tagToTagDtoMapper;
}
@GET @GET
@Path("") @Path("")
public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @StatusCodes({
@DefaultValue("10") @QueryParam("pageSize") int pageSize, @ResponseCode(code = 200, condition = "success"),
@QueryParam("sortBy") String sortBy, @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@DefaultValue("false") @QueryParam("desc") boolean desc) { @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the tags"),
throw new UnsupportedOperationException(); @ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.TAG_COLLECTION)
@TypeHint(CollectionDto.class)
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException, RepositoryNotFoundException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Tags tags = getTags(repositoryService);
if (tags != null && tags.getTags() != null) {
return Response.ok(tagCollectionToDtoMapper.map(namespace, name, tags.getTags())).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Error on getting tag from repository.")
.build();
}
}
} }
@GET @GET
@Path("{id}") @StatusCodes({
public Response get(String id) { @ResponseCode(code = 200, condition = "success"),
throw new UnsupportedOperationException(); @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the tags"),
@ResponseCode(code = 404, condition = "not found, no tag available in the repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.TAG)
@TypeHint(TagDto.class)
@Path("{tagName}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException, RepositoryNotFoundException, TagNotFoundException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
Tags tags = getTags(repositoryService);
if (tags != null && tags.getTags() != null) {
Tag tag = tags.getTags().stream()
.filter(t -> tagName.equals(t.getName()))
.findFirst()
.orElseThrow(TagNotFoundException::new);
return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Error on getting tag from repository.")
.build();
}
}
} }
private Tags getTags(RepositoryService repositoryService) throws IOException {
Repository repository = repositoryService.getRepository();
RepositoryPermissions.read(repository).check();
return repositoryService.getTagsCommand().getTags();
}
} }

View File

@@ -11,6 +11,7 @@ import sonia.scm.repository.Tag;
import javax.inject.Inject; import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.Links.linkingTo;
@Mapper @Mapper
@@ -25,7 +26,9 @@ public abstract class TagToTagDtoMapper {
@AfterMapping @AfterMapping
void appendLinks(@MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) { void appendLinks(@MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) {
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))
.single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())))
.single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())));
target.add(linksBuilder.build()); target.add(linksBuilder.build());
} }
} }

View File

@@ -18,8 +18,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; 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.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.MockitoAnnotations.initMocks;
public class BrowserResultToBrowserResultDtoMapperTest { public class BrowserResultToBrowserResultDtoMapperTest {
@@ -60,6 +63,9 @@ public class BrowserResultToBrowserResultDtoMapperTest {
fileObject2.setPath("/path/object/2"); fileObject2.setPath("/path/object/2");
fileObject2.setDescription("description of file object 2"); fileObject2.setDescription("description of file object 2");
fileObject2.setDirectory(true); fileObject2.setDirectory(true);
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString()))
.thenReturn(new FileObjectDto());
} }
@After @After
@@ -71,7 +77,7 @@ public class BrowserResultToBrowserResultDtoMapperTest {
public void shouldMapAttributesCorrectly() { public void shouldMapAttributesCorrectly() {
BrowserResult browserResult = createBrowserResult(); BrowserResult browserResult = createBrowserResult();
BrowserResultDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar")); BrowserResultDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar"), "path");
assertEqualAttributes(browserResult, dto); assertEqualAttributes(browserResult, dto);
} }
@@ -81,17 +87,25 @@ public class BrowserResultToBrowserResultDtoMapperTest {
BrowserResult browserResult = createBrowserResult(); BrowserResult browserResult = createBrowserResult();
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar"); NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
BrowserResultDto dto = mapper.map(browserResult, namespaceAndName); BrowserResultDto dto = mapper.map(browserResult, namespaceAndName, "path");
verify(fileObjectToFileObjectDtoMapper).map(fileObject1, namespaceAndName, "Revision"); verify(fileObjectToFileObjectDtoMapper).map(fileObject1, namespaceAndName, "Revision");
verify(fileObjectToFileObjectDtoMapper).map(fileObject2, namespaceAndName, "Revision"); verify(fileObjectToFileObjectDtoMapper).map(fileObject2, namespaceAndName, "Revision");
} }
@Test
public void shouldSetLinksCorrectly() {
BrowserResult browserResult = createBrowserResult();
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
BrowserResultDto dto = mapper.map(browserResult, namespaceAndName, "path");
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).contains("path");
}
private BrowserResult createBrowserResult() { private BrowserResult createBrowserResult() {
BrowserResult browserResult = new BrowserResult(); BrowserResult browserResult = new BrowserResult();
browserResult.setTag("Tag");
browserResult.setRevision("Revision"); browserResult.setRevision("Revision");
browserResult.setBranch("Branch");
browserResult.setFiles(createFileObjects()); browserResult.setFiles(createFileObjects());
return browserResult; return browserResult;
@@ -106,8 +120,6 @@ public class BrowserResultToBrowserResultDtoMapperTest {
} }
private void assertEqualAttributes(BrowserResult browserResult, BrowserResultDto dto) { 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()); assertThat(dto.getRevision()).isEqualTo(browserResult.getRevision());
} }

View File

@@ -58,7 +58,7 @@ public class ChangesetRootResourceTest {
private RepositoryServiceFactory serviceFactory; private RepositoryServiceFactory serviceFactory;
@Mock @Mock
private RepositoryService service; private RepositoryService repositoryService;
@Mock @Mock
private LogCommandBuilder logCommandBuilder; private LogCommandBuilder logCommandBuilder;
@@ -83,12 +83,12 @@ public class ChangesetRootResourceTest {
.of(new RepositoryResource(null, null, null, null, null, .of(new RepositoryResource(null, null, null, null, null,
MockProvider.of(changesetRootResource), null, null, null, null)), null); MockProvider.of(changesetRootResource), null, null, null, null)), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource); dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
when(service.getLogCommand()).thenReturn(logCommandBuilder); when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder);
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);
when(subject.isPermitted(any(String.class))).thenReturn(true); when(subject.isPermitted(any(String.class))).thenReturn(true);

View File

@@ -89,8 +89,6 @@ public class SourceRootResourceTest {
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString()).contains("\"revision\":\"revision\""); assertThat(response.getContentAsString()).contains("\"revision\":\"revision\"");
assertThat(response.getContentAsString()).contains("\"tag\":\"tag\"");
assertThat(response.getContentAsString()).contains("\"branch\":\"branch\"");
assertThat(response.getContentAsString()).contains("\"files\":"); assertThat(response.getContentAsString()).contains("\"files\":");
} }
@@ -107,9 +105,7 @@ public class SourceRootResourceTest {
@Test @Test
public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, RevisionNotFoundException { public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, RevisionNotFoundException {
BrowserResult browserResult = new BrowserResult(); BrowserResult browserResult = new BrowserResult();
browserResult.setBranch("abc");
browserResult.setRevision("revision"); browserResult.setRevision("revision");
browserResult.setTag("tag");
FileObject fileObject = new FileObject(); FileObject fileObject = new FileObject();
fileObject.setName("File Object!"); fileObject.setName("File Object!");

View File

@@ -0,0 +1,199 @@
package sonia.scm.api.v2.resources;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
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.api.rest.AuthorizationExceptionMapper;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.Tag;
import sonia.scm.repository.Tags;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.TagsCommandBuilder;
import sonia.scm.web.VndMediaType;
import java.net.URI;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.Silent.class)
public class TagRootResourceTest {
public static final String TAG_PATH = "space/repo/tags/";
public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH;
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 repositoryService;
@Mock
private TagsCommandBuilder tagsCommandBuilder;
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
@InjectMocks
private TagToTagDtoMapperImpl tagToTagDtoMapper;
private TagRootResource tagRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@Before
public void prepareEnvironment() throws Exception {
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider
.of(new RepositoryResource(null, null, null, MockProvider.of(tagRootResource), null,
null, null, null, null, null)), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
when(repositoryService.getTagsCommand()).thenReturn(tagsCommandBuilder);
subjectThreadState.bind();
ThreadContext.bind(subject);
when(subject.isPermitted(any(String.class))).thenReturn(true);
}
@After
public void cleanupContext() {
ThreadContext.unbindSubject();
}
@Test
public void shouldGet404OnMissingTag() throws Exception {
Tags tags = new Tags();
tags.setTags(Lists.emptyList());
when(tagsCommandBuilder.getTags()).thenReturn(tags);
MockHttpRequest request = MockHttpRequest
.get(TAG_URL + "not_existing_tag")
.accept(VndMediaType.TAG);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
}
@Test
public void shouldGetEmptyTagListOnMissingTags() throws Exception {
Tags tags = new Tags();
tags.setTags(Lists.emptyList());
when(tagsCommandBuilder.getTags()).thenReturn(tags);
MockHttpRequest request = MockHttpRequest
.get(TAG_URL)
.accept(VndMediaType.TAG_COLLECTION);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
assertThat(response).isNotNull();
assertThat(response.getContentAsString())
.isNotBlank()
.contains("_links");
}
@Test
public void shouldGet500OnTagCommandError() throws Exception {
when(tagsCommandBuilder.getTags()).thenReturn(null);
MockHttpRequest request = MockHttpRequest
.get(TAG_URL + "not_existing_tag")
.accept(VndMediaType.TAG);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(500, response.getStatus());
request = MockHttpRequest
.get(TAG_URL)
.accept(VndMediaType.TAG_COLLECTION);
response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(500, response.getStatus());
}
@Test
public void shouldGetTags() throws Exception {
Tags tags = new Tags();
String tag1 = "v1.0";
String revision1 = "revision_1234";
String tag2 = "v2.0";
String revision2 = "revision_12345";
tags.setTags(Lists.newArrayList(new Tag(tag1, revision1), new Tag(tag2, revision2)));
when(tagsCommandBuilder.getTags()).thenReturn(tags);
MockHttpRequest request = MockHttpRequest
.get(TAG_URL)
.accept(VndMediaType.TAG_COLLECTION);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
log.info("the content: ", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1)));
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1)));
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
}
@Test
public void shouldGetTag() throws Exception {
Tags tags = new Tags();
String tag1 = "v1.0";
String revision1 = "revision_1234";
String tag2 = "v2.0";
String revision2 = "revision_12345";
tags.setTags(Lists.newArrayList(new Tag(tag1, revision1), new Tag(tag2, revision2)));
when(tagsCommandBuilder.getTags()).thenReturn(tags);
MockHttpRequest request = MockHttpRequest
.get(TAG_URL + tag1)
.accept(VndMediaType.TAG);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1)));
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1)));
request = MockHttpRequest
.get(TAG_URL + tag2)
.accept(VndMediaType.TAG);
response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
log.info("the content: ", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
}
}