diff --git a/pom.xml b/pom.xml
index af38d5175a..788abaca36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,6 +73,7 @@
scm-ui
scm-webapp
scm-server
+ scm-it
diff --git a/scm-it/pom.xml b/scm-it/pom.xml
new file mode 100644
index 0000000000..ab68517a9d
--- /dev/null
+++ b/scm-it/pom.xml
@@ -0,0 +1,173 @@
+
+
+
+ 4.0.0
+
+
+ sonia.scm
+ scm
+ 2.0.0-SNAPSHOT
+
+
+ sonia.scm
+ scm-it
+ jar
+ 2.0.0-SNAPSHOT
+ scm-it
+
+
+
+ sonia.scm
+ scm-core
+ 2.0.0-SNAPSHOT
+
+
+ io.rest-assured
+ rest-assured
+ 3.1.0
+ test
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+ 1.9.0
+
+ http://download.scm-manager.org/licenses/mvn-license.txt
+
+ src/**
+ **/test/**
+
+
+ target/**
+ .hg/**
+ src/main/webapp/resources/extjs/**
+ src/main/webapp/resources/syntaxhighlighter/**
+ src/main/webapp/resources/moment/**
+ **/*.mustache
+
+ true
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.version}
+
+ 8005
+ STOP
+
+
+ scm.home
+ ${scm.home}
+
+
+ scm.stage
+ ${scm.stage}
+
+
+ java.awt.headless
+ true
+
+
+
+ /scm
+
+ ${project.basedir}/src/main/conf/jetty.xml
+ 0
+
+
+
+
+ scm-it
+
+
+
+
+
+
+
+
+ it
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 2.12
+
+
+ sonia/scm/it/*ITCase.java
+
+
+
+
+ integration-test
+
+ integration-test
+
+
+
+ verify
+
+ verify
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.version}
+
+ 8085
+ STOP
+
+
+ scm.home
+ target/scm-it
+
+
+ scm.stage
+ ${scm.stage}
+
+
+ ${project.basedir}/src/main/conf/jetty.xml
+ ${project.basedir}/../scm-webapp/target/scm-webapp.war
+ 0
+ true
+
+
+
+ start-jetty
+ pre-integration-test
+
+ deploy-war
+
+
+
+ stop-jetty
+ post-integration-test
+
+ stop
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scm-it/src/main/conf/jetty.xml b/scm-it/src/main/conf/jetty.xml
new file mode 100644
index 0000000000..ec7ac555c8
--- /dev/null
+++ b/scm-it/src/main/conf/jetty.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ 16384
+ 16384
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java
new file mode 100644
index 0000000000..e5dc7931d3
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java
@@ -0,0 +1,29 @@
+package sonia.scm.it;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+import java.util.regex.Pattern;
+
+class RegExMatcher extends BaseMatcher {
+ public static Matcher matchesPattern(String pattern) {
+ return new RegExMatcher(pattern);
+ }
+
+ private final String pattern;
+
+ private RegExMatcher(String pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("matching to regex pattern \"" + pattern + "\"");
+ }
+
+ @Override
+ public boolean matches(Object o) {
+ return Pattern.compile(pattern).matcher(o.toString()).matches();
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java
new file mode 100644
index 0000000000..6e309e1ba6
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java
@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2010, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ */
+
+
+package sonia.scm.it;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import sonia.scm.util.IOUtil;
+import sonia.scm.web.VndMediaType;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static sonia.scm.it.RegExMatcher.matchesPattern;
+import static sonia.scm.it.RestUtil.createResourceUrl;
+import static sonia.scm.it.RestUtil.given;
+import static sonia.scm.it.RestUtil.readJson;
+
+@RunWith(Parameterized.class)
+public class RepositoriesITCase {
+
+ private final String repositoryType;
+
+ public RepositoriesITCase(String repositoryType) {
+ this.repositoryType = repositoryType;
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection createParameters() {
+ Collection params = new ArrayList<>();
+
+ params.add(new String[]{"git"});
+ params.add(new String[]{"svn"});
+
+ if (IOUtil.search("hg") != null) {
+ params.add(new String[]{"hg"});
+ }
+
+ return params;
+ }
+
+ @After
+ public void cleanup() {
+ TestData.cleanup();
+ }
+
+ @Test
+ public void shouldCreateSuccessfully() throws IOException {
+ String repositoryUrl = createRepository();
+
+ given(VndMediaType.REPOSITORY)
+
+ .when()
+ .get(repositoryUrl)
+
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(
+ "name", equalTo("HeartOfGold-" + repositoryType),
+ "type", equalTo(repositoryType),
+ "creationDate", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z"),
+ "lastModified", is(nullValue()),
+ "_links.self.href", equalTo(repositoryUrl)
+ );
+ }
+
+ @Test
+ public void shouldDeleteSuccessfully() throws IOException {
+ String repositoryUrl = createRepository();
+
+ given(VndMediaType.REPOSITORY)
+
+ .when()
+ .delete(repositoryUrl)
+
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT);
+
+ given(VndMediaType.REPOSITORY)
+
+ .when()
+ .get(repositoryUrl)
+
+ .then()
+ .statusCode(HttpStatus.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void shouldRejectMultipleCreations() throws IOException {
+ String repositoryJson = readJson("repository-" + repositoryType + ".json");
+ createRepository();
+
+ given(VndMediaType.REPOSITORY)
+ .body(repositoryJson)
+
+ .when()
+ .post(createResourceUrl("repositories"))
+
+ .then()
+ .statusCode(HttpStatus.SC_CONFLICT);
+ }
+
+ private String createRepository() throws IOException {
+ return given(VndMediaType.REPOSITORY)
+ .body(readJson("repository-" + repositoryType + ".json"))
+
+ .when()
+ .post(createResourceUrl("repositories"))
+
+ .then()
+ .statusCode(HttpStatus.SC_CREATED)
+ .extract()
+ .header("location");
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java
new file mode 100644
index 0000000000..a902c8985c
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java
@@ -0,0 +1,68 @@
+package sonia.scm.it;
+
+import com.google.common.io.Resources;
+import io.restassured.RestAssured;
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+
+import static java.util.Arrays.asList;
+
+public class RestUtil {
+
+ public static final String BASE_URL = "http://localhost:8081/scm/";
+ public static final String REST_BASE_URL = BASE_URL.concat("api/rest/v2/");
+
+ public static Response lastResponse;
+
+ public static URI createResourceUrl(String url)
+ {
+ return URI.create(REST_BASE_URL).resolve(url);
+ }
+
+ public static String readJson(String jsonFileName) throws IOException {
+ URL url = Resources.getResource(jsonFileName);
+ return Resources.toString(url, Charset.forName("UTF-8"));
+ }
+
+ public static RequestSpecification given(String mediaType) {
+ RequestSpecification requestSpecification = RestAssured.given()
+ .contentType(mediaType)
+ .accept(mediaType)
+ .auth().preemptive().basic("scmadmin", "scmadmin");
+ return wrapRequestSpecification(requestSpecification);
+ }
+
+ private static RequestSpecification wrapRequestSpecification(RequestSpecification requestSpecification) {
+ return (RequestSpecification) Proxy.newProxyInstance(RestUtil.class.getClassLoader(), new Class[]{RequestSpecification.class}, new RequestSpecificationWrapper(requestSpecification));
+ }
+
+ private static class RequestSpecificationWrapper implements InvocationHandler {
+
+ private final RequestSpecification delegate;
+
+ private RequestSpecificationWrapper(RequestSpecification delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
+ if (asList("get", "put", "post", "delete").contains(method.getName())) {
+ lastResponse = (Response) method.invoke(delegate, args);
+ return lastResponse;
+ } else if (method.getReturnType().equals(RequestSpecification.class)) {
+ return wrapRequestSpecification((RequestSpecification) method.invoke(delegate, args));
+ } else {
+ return method.invoke(delegate, args);
+ }
+ }
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java
new file mode 100644
index 0000000000..4127b74b9d
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/TestData.java
@@ -0,0 +1,74 @@
+package sonia.scm.it;
+
+import org.apache.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.web.VndMediaType;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static sonia.scm.it.RestUtil.createResourceUrl;
+import static sonia.scm.it.RestUtil.given;
+
+public class TestData {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestData.class);
+
+ private static final List PROTECTED_USERS = asList("scmadmin", "anonymous");
+
+ public static void cleanup() {
+ cleanupRepositories();
+ cleanupGroups();
+ cleanupUsers();
+ }
+
+ public static void cleanupRepositories() {
+ List repositories = given(VndMediaType.REPOSITORY_COLLECTION)
+ .when()
+ .get(createResourceUrl("repositories"))
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .body().jsonPath().getList("_embedded.repositories._links.self.href");
+ LOG.info("about to delete {} repositories", repositories.size());
+ repositories.forEach(TestData::delete);
+ }
+
+ public static void cleanupGroups() {
+ List groups = given(VndMediaType.GROUP_COLLECTION)
+ .when()
+ .get(createResourceUrl("groups"))
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .body().jsonPath().getList("_embedded.groups._links.self.href");
+ LOG.info("about to delete {} groups", groups.size());
+ groups.forEach(TestData::delete);
+ }
+
+ public static void cleanupUsers() {
+ List users = given(VndMediaType.USER_COLLECTION)
+ .when()
+ .get(createResourceUrl("users"))
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .body().jsonPath().getList("_embedded.users._links.self.href");
+ LOG.info("about to delete {} users", users.size());
+ users.stream().filter(url -> PROTECTED_USERS.stream().noneMatch(url::contains)).forEach(TestData::delete);
+ }
+
+ public static void delete(String url) {
+ given(VndMediaType.REPOSITORY)
+ .when()
+ .delete(url)
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT);
+ LOG.info("deleted {}", url);
+ }
+
+ public static void main(String[] args) {
+ cleanup();
+ }
+}
diff --git a/scm-it/src/test/resources/repository-git.json b/scm-it/src/test/resources/repository-git.json
new file mode 100644
index 0000000000..3fc491ac01
--- /dev/null
+++ b/scm-it/src/test/resources/repository-git.json
@@ -0,0 +1,7 @@
+{
+ "contact": "zaphod.beeblebrox@hitchhiker.com",
+ "description": "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive",
+ "name": "HeartOfGold-git",
+ "archived": false,
+ "type": "git"
+}
diff --git a/scm-it/src/test/resources/repository-hg.json b/scm-it/src/test/resources/repository-hg.json
new file mode 100644
index 0000000000..cf1be4cc2e
--- /dev/null
+++ b/scm-it/src/test/resources/repository-hg.json
@@ -0,0 +1,7 @@
+{
+ "contact": "zaphod.beeblebrox@hitchhiker.com",
+ "description": "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive",
+ "name": "HeartOfGold-hg",
+ "archived": false,
+ "type": "hg"
+}
diff --git a/scm-it/src/test/resources/repository-svn.json b/scm-it/src/test/resources/repository-svn.json
new file mode 100644
index 0000000000..97e2aa6074
--- /dev/null
+++ b/scm-it/src/test/resources/repository-svn.json
@@ -0,0 +1,7 @@
+{
+ "contact": "zaphod.beeblebrox@hitchhiker.com",
+ "description": "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive",
+ "name": "HeartOfGold-svn",
+ "archived": false,
+ "type": "svn"
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/it/GetRepositoriesITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GetRepositoriesITCase.java
deleted file mode 100644
index 6978c897f8..0000000000
--- a/scm-webapp/src/test/java/sonia/scm/it/GetRepositoriesITCase.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.it;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-import sonia.scm.repository.Repository;
-import sonia.scm.repository.RepositoryTestData;
-import sonia.scm.util.IOUtil;
-
-import static org.junit.Assert.*;
-
-import static sonia.scm.it.IntegrationTestUtil.*;
-import static sonia.scm.it.RepositoryITUtil.*;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import com.sun.jersey.api.client.ClientResponse;
-import com.sun.jersey.api.client.WebResource;
-
-import java.io.IOException;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-/**
- *
- * @author Sebastian Sdorra
- */
-@RunWith(Parameterized.class)
-public class GetRepositoriesITCase extends AbstractAdminITCaseBase
-{
-
- /**
- * Constructs ...
- *
- *
- * @param repositoryType
- */
- public GetRepositoriesITCase(String repositoryType)
- {
- this.repositoryType = repositoryType;
- }
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Parameters
- public static Collection createParameters()
- {
- Collection params = new ArrayList();
-
- params.add(new String[] { "git" });
- params.add(new String[] { "svn" });
-
- if (IOUtil.search("hg") != null)
- {
- params.add(new String[] { "hg" });
- }
-
- return params;
- }
-
- /**
- * Method description
- *
- *
- * @throws IOException
- */
- @After
- public void cleanup() throws IOException
- {
- deleteRepository(client, repository.getId());
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testGetById()
- {
- repository = RepositoryTestData.createHeartOfGold(repositoryType);
- repository = createRepository(client, repository);
-
- String id = repository.getId();
-
- assertNotNull(id);
-
- Repository r = getRepositoryById(client, id);
-
- assertEquals(id, r.getId());
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testGetByTypeAndName()
- {
- repository = RepositoryTestData.create42Puzzle(repositoryType);
- testGetByTypeAndName(repository);
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testGetByTypeAndNameWithDirectoryStructure()
- {
- repository =
- RepositoryTestData.createRestaurantAtTheEndOfTheUniverse(repositoryType);
- repository.setName("test/".concat(repository.getName()));
- testGetByTypeAndName(repository);
- }
-
- /**
- * Method description
- *
- *
- * @param repository
- */
- private void testGetByTypeAndName(Repository repo)
- {
- repository = createRepository(client, repo);
-
- String name = repository.getName();
- WebResource wr = createResource(
- client,
- "repositories/".concat(repositoryType).concat(
- "/").concat(name));
- ClientResponse response = wr.get(ClientResponse.class);
-
- assertNotNull(response);
-
- Repository r = response.getEntity(Repository.class);
-
- response.close();
- assertNotNull(r);
- assertEquals(repository.getId(), r.getId());
- assertEquals(repository.getName(), r.getName());
- assertEquals(repository.getType(), r.getType());
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private Repository repository;
-
- /** Field description */
- private String repositoryType;
-}