Skip to content
Closed
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
29 changes: 18 additions & 11 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -642,18 +642,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</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>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5-fluent</artifactId>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -1163,6 +1158,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,11 @@

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.apache.hc.client5.http.fluent.Executor;
import org.apache.hc.client5.http.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 @@ -44,14 +42,15 @@
import java.io.IOException;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.hc.core5.http.HttpStatus.SC_CREATED;
import static org.apache.hc.core5.http.HttpStatus.SC_OK;
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 +66,21 @@ 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 Executor executor = AbstractHttpTest.createAuthenticatedExecutor(
existWebServer, TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD);

// store document
final HttpResponse storeResponse = executor.execute(
assertEquals(SC_CREATED, executeForStatus(executor,
Request
.Put(docUri)
.put(docUri)
.addHeader("Content-Type", "application/xml")
.bodyByteArray(cdata_xml.getBytes(UTF_8))
).returnResponse();
assertEquals(SC_CREATED, storeResponse.getStatusLine().getStatusCode());
));

// 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 HttpResponseResult retrieved = executeForStatusAndBody(executor, Request.get(docUri));
assertEquals(SC_OK, retrieved.statusCode());
assertEquals(cdata_xml, retrieved.body());
}

@Test
Expand Down
164 changes: 144 additions & 20 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.apache.hc.client5.http.fluent.Executor;
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.exist.TestUtils;
import org.exist.test.ExistWebServer;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static org.junit.Assert.assertEquals;

/**
* @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
*/
public abstract class AbstractHttpTest {

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

/**
* Get the Server URI.
*
Expand Down Expand Up @@ -73,6 +84,67 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
return getServerUri(existWebServer) + "/apps";
}

/**
* Create an {@link HttpHost} for the given eXist-db Web Server.
*
* @param existWebServer the eXist-db Web Server.
*
* @return the HTTP host.
*/
public static HttpHost getHttpHost(final ExistWebServer existWebServer) {
return new HttpHost("http", "localhost", existWebServer.getPort());
}

private 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 sends preemptive HTTP Basic authentication.
*
* <p>HC5's fluent {@link Executor} auth helpers do not always attach credentials to requests
* under the {@code /exist/...} context path; the request interceptor ensures the
* {@code Authorization} header is present on the first request.</p>
*
* @param existWebServer the eXist-db Web Server.
* @param user the user name.
* @param password the password.
*
* @return a closable HTTP client.
*/
public static CloseableHttpClient createAuthenticatedClient(
final ExistWebServer existWebServer,
final String user,
final String password) {
final String authorizationHeader = basicAuthorizationHeader(user, password);

return HttpClients.custom()
.addRequestInterceptorFirst((request, entity, context) -> {
if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
}
})
.disableAutomaticRetries()
.build();
}

/**
* Create an HTTP executor that sends preemptive HTTP Basic authentication.
*
* @param existWebServer the eXist-db Web Server.
* @param user the user name.
* @param password the password.
*
* @return an executor backed by {@link #createAuthenticatedClient(ExistWebServer, String, String)}.
*/
public static Executor createAuthenticatedExecutor(
final ExistWebServer existWebServer,
final String user,
final String password) {
return Executor.newInstance(createAuthenticatedClient(existWebServer, user, password));
}

/**
* Execute a function with a HTTP Client.
*
Expand All @@ -83,13 +155,9 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
*
* @throws IOException if an I/O error occurs
*/
protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException> fn) throws IOException {
try (final CloseableHttpClient client = HttpClientBuilder
.create()
protected static <T> T withHttpClient(final FunctionE<CloseableHttpClient, T, IOException> fn) throws IOException {
try (final CloseableHttpClient client = HttpClients.custom()
.disableAutomaticRetries()
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.STANDARD)
.build())
.build()) {
return fn.apply(client);
}
Expand All @@ -107,12 +175,68 @@ protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException
* @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);
});
try (final CloseableHttpClient client = createAuthenticatedClient(
existWebServer, TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)) {
return fn.apply(Executor.newInstance(client));
}
}

/**
* Execute a request and return its status code, closing the response.
*/
protected static int executeForStatus(final Executor executor, final Request request) throws IOException {
try (ClassicHttpResponse response = (ClassicHttpResponse) executor.execute(request).returnResponse()) {
return response.getCode();
}
}

/**
* Execute a request and return its status code, closing the response.
*/
protected static int executeForStatus(final Request request) throws IOException {
try (ClassicHttpResponse response = (ClassicHttpResponse) request.execute().returnResponse()) {
return response.getCode();
}
}

/**
* Execute a request and return status code and body, closing the response.
*/
public static HttpResponseResult executeForStatusAndBody(final Executor executor, final Request request)
throws IOException {
try (ClassicHttpResponse response = (ClassicHttpResponse) executor.execute(request).returnResponse()) {
return new HttpResponseResult(response.getCode(), readResponseBody(response));
}
}

/**
* Execute a request and return status code and body, closing the response.
*/
public static HttpResponseResult executeForStatusAndBody(final Request request) throws IOException {
try (ClassicHttpResponse response = (ClassicHttpResponse) request.execute().returnResponse()) {
return new HttpResponseResult(response.getCode(), readResponseBody(response));
}
}

protected static String readResponseBody(final ClassicHttpResponse response) throws IOException {
if (response.getEntity() == null) {
return "";
}
try (UnsynchronizedByteArrayOutputStream baos = new UnsynchronizedByteArrayOutputStream()) {
response.getEntity().writeTo(baos);
return baos.toString(StandardCharsets.UTF_8);
}
}

/**
* Execute a fluent request and assert status and body, closing the response.
*/
public static void assertRequestResponse(
final Request request,
final int expectedStatus,
final String expectedBody) throws IOException {
final HttpResponseResult result = executeForStatusAndBody(request);
assertEquals(expectedStatus, result.statusCode());
assertEquals(expectedBody, result.body());
}
}
Loading
Loading