Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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