Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ updates:
update-types:
- patch
- minor
milton-webdav:
patterns:
- "org.exist-db.thirdparty.com.ettrema:*"
update-types:
- patch
- minor
maven-plugins-apache:
patterns:
- "org.apache.maven.plugins:*"
Expand Down
12 changes: 12 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ Add all three to `~/.m2/settings.xml`. **The same GitHub PAT (with the `read:pac

CI generates the same shape from secrets via `.github/actions/maven-github-settings/action.yml`.

If a previous resolve failed with 401, Maven may cache the failure as `*.lastUpdated` under `~/.m2/repository/`. After fixing `settings.xml`, delete that artifact directory or add `-U` on the next build.

Example: verify Jackrabbit resolves after configuring auth:

```bash
rm -rf ~/.m2/repository/org/exist-db/thirdparty/org/apache/jackrabbit/jackrabbit-webdav/2.22.3-jakarta-ee10
mvn dependency:get \
-Dartifact=org.exist-db.thirdparty.org.apache.jackrabbit:jackrabbit-webdav:2.22.3-jakarta-ee10 \
-U -Ddependency-check.skip=true
```

Further build options can be found at: [eXist-db Build Documentation](http://www.exist-db.org/exist/apps/doc/exist-building.xml "How to build eXist") and on the workflow files of this repo.

### Running tests locally
Expand All @@ -75,6 +86,7 @@ From the repo root:
- **All tests:** `mvn -V -B verify -Ddependency-check.skip -Dlicense.skip`
- **exist-core only:** add `--projects exist-core --also-make` to the above
- **Single test class:** `mvn -Dtest=fully.qualified.TestClass test --projects exist-core --also-make`
- **WebDAV round-trip tests:** `mvn test -pl extensions/webdav --also-make -Dtest=org.exist.webdav.WebDavRoundTripTest -Dsurefire.failIfNoSpecifiedTests=false` (requires `github-jackrabbit-webdav-jakarta` auth; litmus compliance runs in Docker CI)

**NOTE:**
In the above example, we switched the current (checked-out) branch from `develop` to `master`. We use the [GitFlow for eXist-db](#contributing-to-exist) process:
Expand Down
37 changes: 17 additions & 20 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,11 @@
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.exist-db</groupId>
<artifactId>exist-jetty-config</artifactId>
Expand Down Expand Up @@ -575,11 +580,6 @@
<artifactId>junit-toolbox</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>

<!-- needed for starting up a jetty server -->
<dependency>
Expand Down Expand Up @@ -641,21 +641,6 @@
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down Expand Up @@ -1163,6 +1148,18 @@ The BaseX Team. The original license statement is also included below.]]></pream
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@

package org.exist.dom.persistent;

import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.exist.TestUtils;
import org.exist.http.AbstractHttpTest;
import org.exist.test.ExistWebServer;
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.exist.xmldb.DatabaseImpl;
import org.exist.xmldb.XmldbURI;
import org.junit.ClassRule;
Expand All @@ -42,16 +38,20 @@
import org.xmldb.api.modules.XMLResource;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;

import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.junit.Assert.*;

/**
* Tests for retrieving a document containing CDATA via
* various APIs.
*/
public class CDataIntergationTest {
public class CDataIntergationTest extends AbstractHttpTest {

@ClassRule
public static final ExistWebServer existWebServer = new ExistWebServer(true, false, true, true);
Expand All @@ -67,29 +67,22 @@ public void cdataRestApi() throws IOException {
final String uri = "http://localhost:" + existWebServer.getPort() + "/exist/rest/db";
final String docUri = uri + "/rest-cdata-test.xml";

final Executor executor = Executor
.newInstance()
.auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
.authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
final HttpClient client = newHttpClient();

// store document
final HttpResponse storeResponse = executor.execute(
Request
.Put(docUri)
.addHeader("Content-Type", "application/xml")
.bodyByteArray(cdata_xml.getBytes(UTF_8))
).returnResponse();
assertEquals(SC_CREATED, storeResponse.getStatusLine().getStatusCode());
final HttpRequest putRequest = authenticatedRequest(URI.create(docUri), TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
.header("Content-Type", "application/xml")
.PUT(HttpRequest.BodyPublishers.ofByteArray(cdata_xml.getBytes(UTF_8)))
.build();
assertEquals(HTTP_CREATED, executeForStatus(client, putRequest));

// retrieve document
final HttpResponse retrieveResponse = executor.execute(
Request
.Get(docUri)
).returnResponse();
try (final UnsynchronizedByteArrayOutputStream baos = new UnsynchronizedByteArrayOutputStream()) {
retrieveResponse.getEntity().writeTo(baos);
assertEquals(cdata_xml, baos.toString(UTF_8));
}
final HttpRequest getRequest = authenticatedRequest(URI.create(docUri), TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
.GET()
.build();
final HttpResponseResult retrieved = executeForStatusAndBody(client, getRequest);
assertEquals(HTTP_OK, retrieved.statusCode());
assertEquals(cdata_xml, retrieved.body());
}

@Test
Expand Down
154 changes: 118 additions & 36 deletions exist-core/src/test/java/org/exist/http/AbstractHttpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,34 @@
package org.exist.http;

import com.evolvedbinary.j8fu.function.FunctionE;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Executor;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.exist.TestUtils;
import org.exist.test.ExistWebServer;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.exist.test.ExistWebServer;

import static org.junit.Assert.assertEquals;

/**
* Shared HTTP test infrastructure, built on the JDK {@link java.net.http.HttpClient} (no Apache
* HttpClient). Provides the eXist-db server URIs, preemptive HTTP Basic authentication, and small
* helpers for executing a request and reading the status / body.
*
* @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
*/
public abstract class AbstractHttpTest {

/**
* HTTP status and body from a single request execution.
*/
public record HttpResponseResult(int statusCode, String body) {
}

/**
* Get the Server URI.
*
Expand Down Expand Up @@ -74,45 +85,116 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
}

/**
* Execute a function with a HTTP Client.
* Build a preemptive HTTP Basic {@code Authorization} header value.
*
* @param <T> the return type of the <code>fn</code> function.
* @param fn the function which accepts the HTTP Client.
* @param user the user name.
* @param password the password.
*
* @return the result of the <code>fn</code> function.
* @return the {@code Authorization} header value, e.g. {@code "Basic dXNlcjpwYXNz"}.
*/
public static String basicAuthorizationHeader(final String user, final String password) {
return "Basic " + Base64.getEncoder().encodeToString(
(user + ":" + password).getBytes(StandardCharsets.UTF_8));
}

/**
* Create an HTTP client that follows redirects (matching the previous fluent client's default).
*
* @throws IOException if an I/O error occurs
* @return a new {@link HttpClient}.
*/
protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException> fn) throws IOException {
try (final CloseableHttpClient client = HttpClientBuilder
.create()
.disableAutomaticRetries()
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.STANDARD)
.build())
.build()) {
return fn.apply(client);
}
public static HttpClient newHttpClient() {
return HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}

/**
* Start building a request to the given URI with a preemptive HTTP Basic {@code Authorization}
* header already set.
*
* @param uri the request URI.
* @param user the user name.
* @param password the password.
*
* @return a request builder carrying the {@code Authorization} header.
*/
public static HttpRequest.Builder authenticatedRequest(final URI uri, final String user, final String password) {
return HttpRequest.newBuilder(uri)
.header("Authorization", basicAuthorizationHeader(user, password));
}

/**
* Execute a request and return its status code.
*
* @param client the HTTP client.
* @param request the request.
*
* @return the HTTP status code.
*
* @throws IOException if an I/O error occurs.
*/
public static int executeForStatus(final HttpClient client, final HttpRequest request) throws IOException {
return send(client, request, HttpResponse.BodyHandlers.discarding()).statusCode();
}

/**
* Execute a request and return its status code and body.
*
* @param client the HTTP client.
* @param request the request.
*
* @return the status code and body.
*
* @throws IOException if an I/O error occurs.
*/
public static HttpResponseResult executeForStatusAndBody(final HttpClient client, final HttpRequest request)
throws IOException {
final HttpResponse<String> response = send(client, request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return new HttpResponseResult(response.statusCode(), response.body());
}

/**
* Execute a request and assert its status and body.
*
* @param client the HTTP client.
* @param request the request.
* @param expectedStatus the expected HTTP status code.
* @param expectedBody the expected response body.
*
* @throws IOException if an I/O error occurs.
*/
public static void assertRequestResponse(final HttpClient client, final HttpRequest request,
final int expectedStatus, final String expectedBody) throws IOException {
final HttpResponseResult result = executeForStatusAndBody(client, request);
assertEquals(expectedStatus, result.statusCode());
assertEquals(expectedBody, result.body());
}

/**
* Execute a function with a HTTP Executor.
* Execute a function with a new HTTP Client.
*
* @param <T> the return type of the <code>fn</code> function.
* @param existWebServer the eXist-db Web Server.
* @param fn the function which accepts the HTTP Executor.
* @param fn the function which accepts the HTTP Client.
*
* @return the result of the <code>fn</code> function.
*
* @throws IOException if an I/O error occurs
* @throws IOException if an I/O error occurs.
*/
protected static <T> T withHttpExecutor(final ExistWebServer existWebServer, final FunctionE<Executor, T, IOException> fn) throws IOException {
return withHttpClient(client -> {
final Executor executor = Executor
.newInstance(client)
.auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
.authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
return fn.apply(executor);
});
protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException> fn) throws IOException {
return fn.apply(newHttpClient());
}

/**
* Send a request, translating the checked {@link InterruptedException} thrown by
* {@link HttpClient#send} into an {@link IOException} so callers keep a single checked type.
*/
private static <T> HttpResponse<T> send(final HttpClient client, final HttpRequest request,
final HttpResponse.BodyHandler<T> bodyHandler) throws IOException {
try {
return client.send(request, bodyHandler);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while awaiting HTTP response", e);
}
}
}
Loading
Loading