summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-12 00:49:49 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-12 00:49:49 +0200
commitb2e9142ea9a61af8c6e55a39a392136306dc0312 (patch)
tree918e2fc6308b9ed040d8470c21a4731e9766a0a2 /test
parentbfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (diff)
towards a Jetty 9 decoupling baseline for the Jetty 12 migration
Diffstat (limited to 'test')
-rw-r--r--test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java32
-rw-r--r--test/java/net/yacy/http/HttpServerBootstrapConfigTest.java25
-rw-r--r--test/java/net/yacy/http/Jetty9LoggingFacadeTest.java16
-rw-r--r--test/java/net/yacy/http/ProxyAccessPolicyTest.java20
-rw-r--r--test/java/net/yacy/http/Slf4jJulBridgeTest.java59
-rwxr-xr-xtest/jetty-auth-smoke-test.sh66
-rwxr-xr-xtest/jetty-peer-start-smoke-test.sh49
-rwxr-xr-xtest/jetty-smoke-test.sh109
-rwxr-xr-xtest/jetty-solr-dependency-guard.sh72
-rw-r--r--test/jetty/RelocateJettyPackages.java65
-rwxr-xr-xtest/proxy-smoke-test.sh28
-rwxr-xr-xtest/remote-solr-smoke-test.sh30
12 files changed, 536 insertions, 35 deletions
diff --git a/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java b/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java
new file mode 100644
index 000000000..480579150
--- /dev/null
+++ b/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java
@@ -0,0 +1,32 @@
+package net.yacy.cora.federate.solr.connector;
+
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.params.ModifiableSolrParams;
+
+import net.yacy.cora.federate.solr.instance.RemoteInstance;
+
+/** Command-line integration probe for YaCY's Apache-based remote Solr path. */
+public final class RemoteSolrSmoke {
+
+ private RemoteSolrSmoke() {
+ }
+
+ public static void main(final String[] args) throws Exception {
+ if (args.length != 1) {
+ throw new IllegalArgumentException("usage: RemoteSolrSmoke SOLR_BASE_URL");
+ }
+ final RemoteInstance instance = new RemoteInstance(
+ args[0], null, "collection1", 10_000, false, Long.MAX_VALUE, false);
+ try {
+ final RemoteSolrConnector connector = new RemoteSolrConnector(instance, false);
+ final QueryResponse response = connector.getResponseByParams(
+ new ModifiableSolrParams().set("q", "*:*").set("rows", 0));
+ if (response.getResults() == null) {
+ throw new IllegalStateException("remote Solr response has no result list");
+ }
+ System.out.println("Remote Solr numFound=" + response.getResults().getNumFound());
+ } finally {
+ instance.close();
+ }
+ }
+}
diff --git a/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java
new file mode 100644
index 000000000..7f2f71120
--- /dev/null
+++ b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java
@@ -0,0 +1,25 @@
+package net.yacy.http;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HttpServerBootstrapConfigTest {
+
+ @Test
+ public void testAcceptorCountIsClamped() {
+ Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(1));
+ Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(2));
+ Assert.assertEquals(2, HttpServerBootstrapConfig.acceptorCountFor(4));
+ Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(8));
+ Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(64));
+ }
+
+ @Test
+ public void testFixedConnectorLimits() {
+ Assert.assertEquals(16_384, HttpServerBootstrapConfig.REQUEST_HEADER_SIZE);
+ Assert.assertEquals(9_000L, HttpServerBootstrapConfig.CONNECTOR_IDLE_TIMEOUT_MILLIS);
+ Assert.assertEquals(128, HttpServerBootstrapConfig.ACCEPT_QUEUE_SIZE);
+ Assert.assertEquals(4_096, HttpServerBootstrapConfig.REQUEST_INFLATE_BUFFER_SIZE);
+ Assert.assertEquals(-1, HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE);
+ }
+}
diff --git a/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java b/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java
new file mode 100644
index 000000000..bba2b6924
--- /dev/null
+++ b/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java
@@ -0,0 +1,16 @@
+package net.yacy.http;
+
+import static org.junit.Assert.assertEquals;
+
+import org.eclipse.jetty.util.log.Log;
+import org.junit.Test;
+
+/** Jetty 9 baseline only; replace this test when Jetty9HttpServerImpl is removed. */
+public class Jetty9LoggingFacadeTest {
+
+ @Test
+ public void jetty9UsesItsSlf4jFacade() {
+ assertEquals("org.eclipse.jetty.util.log.Slf4jLog",
+ Log.getLogger("org.eclipse.jetty.yacy.logging.test").getClass().getName());
+ }
+}
diff --git a/test/java/net/yacy/http/ProxyAccessPolicyTest.java b/test/java/net/yacy/http/ProxyAccessPolicyTest.java
new file mode 100644
index 000000000..ce67c6ce3
--- /dev/null
+++ b/test/java/net/yacy/http/ProxyAccessPolicyTest.java
@@ -0,0 +1,20 @@
+package net.yacy.http;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ProxyAccessPolicyTest {
+
+ @Test
+ public void testConfiguredClientPatterns() {
+ Assert.assertTrue(ProxyAccessPolicy.isClientAllowed("*", "198.51.100.7"));
+ Assert.assertTrue(ProxyAccessPolicy.isClientAllowed(
+ "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "127.0.0.1"));
+ Assert.assertTrue(ProxyAccessPolicy.isClientAllowed(
+ "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "192.168.2.15"));
+ Assert.assertFalse(ProxyAccessPolicy.isClientAllowed(
+ "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "198.51.100.7"));
+ Assert.assertFalse(ProxyAccessPolicy.isClientAllowed(null, "127.0.0.1"));
+ Assert.assertFalse(ProxyAccessPolicy.isClientAllowed("127\\.0\\.0\\.1", null));
+ }
+}
diff --git a/test/java/net/yacy/http/Slf4jJulBridgeTest.java b/test/java/net/yacy/http/Slf4jJulBridgeTest.java
new file mode 100644
index 000000000..b1448eae6
--- /dev/null
+++ b/test/java/net/yacy/http/Slf4jJulBridgeTest.java
@@ -0,0 +1,59 @@
+package net.yacy.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+/** Version-neutral contract for the public SLF4J 2 to JUL logging path. */
+public class Slf4jJulBridgeTest {
+
+ @Test
+ public void publicSlf4jProviderRoutesJettyNamespaceToJul() {
+ assertEquals("org.slf4j.jul.JDK14LoggerFactory",
+ LoggerFactory.getILoggerFactory().getClass().getName());
+
+ final String loggerName = "org.eclipse.jetty.yacy.logging.test";
+ final Logger julLogger = Logger.getLogger(loggerName);
+ final Level previousLevel = julLogger.getLevel();
+ final boolean previousUseParentHandlers = julLogger.getUseParentHandlers();
+ final AtomicReference<LogRecord> received = new AtomicReference<>();
+ final Handler capture = new Handler() {
+ @Override
+ public void publish(final LogRecord record) {
+ received.set(record);
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() {
+ }
+ };
+
+ try {
+ julLogger.setUseParentHandlers(false);
+ julLogger.setLevel(Level.INFO);
+ capture.setLevel(Level.ALL);
+ julLogger.addHandler(capture);
+
+ LoggerFactory.getLogger(loggerName).info("Jetty SLF4J to JUL bridge test");
+
+ assertTrue("SLF4J log record did not reach java.util.logging", received.get() != null);
+ assertEquals("Jetty SLF4J to JUL bridge test", received.get().getMessage());
+ } finally {
+ julLogger.removeHandler(capture);
+ julLogger.setLevel(previousLevel);
+ julLogger.setUseParentHandlers(previousUseParentHandlers);
+ }
+ }
+}
diff --git a/test/jetty-auth-smoke-test.sh b/test/jetty-auth-smoke-test.sh
new file mode 100755
index 000000000..4970a7e47
--- /dev/null
+++ b/test/jetty-auth-smoke-test.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env sh
+
+# Authentication contract for the embedded server. A remote URL must really
+# reach the peer through a non-loopback socket; forwarded headers are not used.
+
+set -eu
+
+base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090}
+remote_base_url=${YACY_SMOKE_REMOTE_BASE_URL:-}
+protected_path=${YACY_SMOKE_PROTECTED_PATH:-/ConfigAccounts_p.html}
+conf=${YACY_SMOKE_CONF:-DATA/SETTINGS/yacy.conf}
+timeout=${YACY_SMOKE_TIMEOUT:-30}
+require_complete=${YACY_SMOKE_REQUIRE_COMPLETE:-false}
+
+case "$require_complete" in
+ true|false) ;;
+ *) echo "FAIL: YACY_SMOKE_REQUIRE_COMPLETE must be true or false" >&2; exit 2 ;;
+esac
+
+[ -f "$conf" ] || { echo "FAIL: missing peer configuration: $conf" >&2; exit 2; }
+
+status() {
+ curl --silent --show-error --max-time "$timeout" --output /dev/null \
+ --write-out '%{http_code}' "$@"
+}
+
+localhost_access=$(sed -n 's/^adminAccountForLocalhost=//p' "$conf" | head -n 1)
+local_status=$(status "$base_url$protected_path")
+if [ "$localhost_access" = "true" ]; then
+ [ "$local_status" = 200 ] || { echo "FAIL: localhost bypass returned $local_status" >&2; exit 1; }
+else
+ [ "$local_status" = 401 ] || { echo "FAIL: protected localhost request returned $local_status" >&2; exit 1; }
+fi
+echo "ok 1 - configured localhost access rule"
+
+YACY_DATA_PATH=$(CDPATH= cd -- "$(dirname "$conf")/.." && pwd)
+export YACY_DATA_PATH
+bin/apicall.sh 'ConfigAccounts_p.html' >/dev/null
+echo "ok 2 - bin/apicall.sh localhost authentication"
+
+if [ -n "${YACY_SMOKE_ADMIN_USER:-}" ] && [ -n "${YACY_SMOKE_ADMIN_PASSWORD:-}" ]; then
+ authenticated_status=$(status --user "$YACY_SMOKE_ADMIN_USER:$YACY_SMOKE_ADMIN_PASSWORD" \
+ "$base_url$protected_path")
+ [ "$authenticated_status" = 200 ] || { echo "FAIL: administrator login returned $authenticated_status" >&2; exit 1; }
+ echo "ok 3 - administrator credentials"
+else
+ if [ "$require_complete" = true ]; then
+ echo "FAIL: complete acceptance requires YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD" >&2
+ exit 1
+ fi
+ echo "ok 3 # SKIP - set YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD"
+fi
+
+if [ -n "$remote_base_url" ]; then
+ remote_status=$(status "$remote_base_url$protected_path")
+ [ "$remote_status" = 401 ] || { echo "FAIL: remote protected request returned $remote_status" >&2; exit 1; }
+ echo "ok 4 - remote request does not receive localhost bypass"
+else
+ if [ "$require_complete" = true ]; then
+ echo "FAIL: complete acceptance requires YACY_SMOKE_REMOTE_BASE_URL over a real non-loopback path" >&2
+ exit 1
+ fi
+ echo "ok 4 # SKIP - set YACY_SMOKE_REMOTE_BASE_URL to a real non-loopback path"
+fi
+
+echo "PASS: embedded-server authentication contract"
diff --git a/test/jetty-peer-start-smoke-test.sh b/test/jetty-peer-start-smoke-test.sh
new file mode 100755
index 000000000..148eb8678
--- /dev/null
+++ b/test/jetty-peer-start-smoke-test.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env sh
+
+# Lifecycle gate owned by an external isolated-peer harness. Commands are
+# explicit because this script must never start or stop the developer's normal
+# DATA directory by guessing.
+
+set -eu
+
+[ -n "${YACY_SMOKE_START_COMMAND:-}" ] && [ -n "${YACY_SMOKE_STOP_COMMAND:-}" ] || {
+ echo "SKIP: set YACY_SMOKE_START_COMMAND and YACY_SMOKE_STOP_COMMAND for an isolated DATA directory" >&2
+ exit 2
+}
+
+base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090}
+timeout=${YACY_SMOKE_TIMEOUT:-60}
+started=false
+cleanup() {
+ if [ "$started" = true ]; then
+ sh -c "$YACY_SMOKE_STOP_COMMAND"
+ fi
+}
+trap cleanup EXIT HUP INT TERM
+
+sh -c "$YACY_SMOKE_START_COMMAND"
+started=true
+
+attempt=0
+while [ "$attempt" -lt "$timeout" ]; do
+ if curl --silent --fail --max-time 2 "$base_url/api/version.xml" >/dev/null 2>&1; then
+ break
+ fi
+ attempt=$((attempt + 1))
+ sleep 1
+done
+[ "$attempt" -lt "$timeout" ] || { echo "FAIL: isolated peer did not start" >&2; exit 1; }
+
+test/jetty-smoke-test.sh "$base_url"
+curl --silent --show-error --fail --max-time 30 \
+ "$base_url/solr/collection1/select?q=*:*&rows=0&wt=json" \
+ | grep -F 'numFound' >/dev/null || {
+ echo "FAIL: embedded Solr query failed" >&2
+ exit 1
+ }
+echo "ok - embedded Solr query"
+
+cleanup
+started=false
+trap - EXIT HUP INT TERM
+echo "PASS: isolated peer start, embedded Solr and clean stop"
diff --git a/test/jetty-smoke-test.sh b/test/jetty-smoke-test.sh
index ab53a0d8c..0ff61ccc2 100755
--- a/test/jetty-smoke-test.sh
+++ b/test/jetty-smoke-test.sh
@@ -53,21 +53,35 @@ fail() {
request() {
method=$1
path=$2
- shift 2
+ expected_status=$3
+ shift 3
: > "$response_headers"
: > "$response_body"
- status=$(curl --silent --show-error \
- --max-time "$curl_timeout" \
- --request "$method" \
- --dump-header "$response_headers" \
- --output "$response_body" \
- --write-out '%{http_code}' \
- "$@" \
- "$base_url$path") || fail "$method $path could not be requested"
+ if [ "$method" = HEAD ]; then
+ status=$(curl --silent --show-error \
+ --max-time "$curl_timeout" \
+ --head \
+ --dump-header "$response_headers" \
+ --output /dev/null \
+ --write-out '%{http_code}' \
+ "$@" \
+ "$base_url$path") || fail "$method $path could not be requested"
+ : > "$response_body"
+ else
+ status=$(curl --silent --show-error \
+ --max-time "$curl_timeout" \
+ --request "$method" \
+ --dump-header "$response_headers" \
+ --output "$response_body" \
+ --write-out '%{http_code}' \
+ "$@" \
+ "$base_url$path") || fail "$method $path could not be requested"
+ fi
- [ "$status" = "200" ] || fail "$method $path returned HTTP $status"
+ [ "$status" = "$expected_status" ] || \
+ fail "$method $path returned HTTP $status (expected $expected_status)"
}
assert_header_contains() {
@@ -109,38 +123,101 @@ while [ "$attempt" -lt 30 ]; do
done
[ "$attempt" -lt 30 ] || fail "YaCy did not become ready within 30 seconds"
-request GET /api/version.xml
+request GET /api/version.xml 200
assert_header_contains Content-Type text/xml
assert_body_contains '<version>'
assert_body_contains '<buildVersion>'
pass "reflection-backed XML endpoint"
-request GET /env/grafics/YaCyLogo2012.svg
+request GET /env/grafics/YaCyLogo2012.svg 200
assert_header_contains Content-Type image/svg+xml
assert_body_contains '<svg'
pass "static resource"
-request GET /index.html
+cp "$response_body" "$work_dir/static-full"
+static_length=$(wc -c < "$work_dir/static-full" | tr -d ' ')
+request HEAD /env/grafics/YaCyLogo2012.svg 200
+assert_header_contains Content-Type image/svg+xml
+assert_header_contains Content-Length "$static_length"
+[ ! -s "$response_body" ] || fail "HEAD response contains a body"
+pass "HEAD without response body"
+
+request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-9'
+assert_header_contains Content-Range "bytes 0-9/$static_length"
+[ "$(wc -c < "$response_body" | tr -d ' ')" -eq 10 ] || fail "range body is not 10 bytes"
+head -c 10 "$work_dir/static-full" > "$work_dir/static-prefix"
+cmp "$work_dir/static-prefix" "$response_body" >/dev/null 2>&1 || fail "range body has unexpected bytes"
+pass "single byte range"
+
+request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-4,48-51'
+assert_header_contains Content-Type 'multipart/byteranges; boundary='
+assert_body_contains "Content-Range: bytes 0-4/$static_length"
+assert_body_contains "Content-Range: bytes 48-51/$static_length"
+assert_body_contains '<?xml'
+assert_body_contains 'YaCy'
+multipart_boundary=$(tr -d '\r' < "$response_headers" | awk -F 'boundary=' '
+ tolower($0) ~ /^content-type: multipart\/byteranges/ { print $2 }
+' | tail -n 1)
+[ -n "$multipart_boundary" ] || fail "multipart range response has no boundary"
+tail -c 80 "$response_body" | grep -F -- "--$multipart_boundary--" >/dev/null 2>&1 || \
+ fail "multipart range response has no closing boundary"
+pass "multipart byte ranges"
+
+request GET /env/grafics/YaCyLogo2012.svg 416 --header "Range: bytes=$static_length-"
+assert_header_contains Content-Range "bytes */$static_length"
+pass "unsatisfiable byte range"
+
+# YaCy deliberately suppresses Last-Modified on static responses to control its
+# cache policy, but still implements If-Modified-Since against the resource.
+request GET /env/grafics/YaCyLogo2012.svg 304 \
+ --header 'If-Modified-Since: Thu, 31 Dec 2099 23:59:59 GMT'
+[ ! -s "$response_body" ] || fail "304 response contains a body"
+pass "If-Modified-Since"
+
+request GET /index.html 200
assert_header_contains Content-Type text/html
assert_body_contains '<!DOCTYPE html>'
assert_body_contains '<html'
pass "rendered HTML template"
-request GET '/suggest.json?q=jetty-smoke'
+request GET '/suggest.json?q=jetty-smoke' 200
assert_header_contains Content-Type application/json
assert_header_contains Access-Control-Allow-Origin '*'
assert_body_contains '["jetty-smoke",['
pass "reflection-backed JSON endpoint and CORS header"
-request POST /api/version.xml \
+request POST /api/version.xml 200 \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'smoke=post'
assert_header_contains Content-Type text/xml
assert_body_contains '<version>'
pass "URL-encoded POST dispatch"
-request OPTIONS /api/version.xml
+request OPTIONS /api/version.xml 200
assert_header_contains Allow 'GET,HEAD,POST,OPTIONS'
pass "OPTIONS method contract"
+request GET /this-resource-must-not-exist-yacy-jetty-smoke 404
+assert_header_contains Content-Type text/html
+assert_body_contains 'YaCy '
+pass "YaCy 404 error page"
+
+request GET /env/grafics/YaCyLogo2012.svg 200 \
+ --header 'Accept-Encoding: gzip' \
+ --raw
+assert_header_contains Content-Encoding gzip
+gzip -dc "$response_body" > "$work_dir/gzip-decoded" || fail "gzip response cannot be decompressed"
+cmp "$work_dir/static-full" "$work_dir/gzip-decoded" >/dev/null 2>&1 || \
+ fail "decompressed response differs from the uncompressed resource"
+pass "gzip response compression"
+
+printf 'q=jetty-gzip-smoke' > "$work_dir/gzip-request-form"
+gzip -c "$work_dir/gzip-request-form" > "$work_dir/gzip-request-body"
+request POST /suggest.json 200 \
+ --header 'Content-Type: application/x-www-form-urlencoded' \
+ --header 'Content-Encoding: gzip' \
+ --data-binary "@$work_dir/gzip-request-body"
+assert_body_contains '["jetty-gzip-smoke",['
+pass "gzip request decompression"
+
echo "PASS: $checks embedded-server checks succeeded."
diff --git a/test/jetty-solr-dependency-guard.sh b/test/jetty-solr-dependency-guard.sh
index 0f2fad8a2..21e208194 100755
--- a/test/jetty-solr-dependency-guard.sh
+++ b/test/jetty-solr-dependency-guard.sh
@@ -42,14 +42,72 @@ for artifact in http2-client http2-common http2-http-client-transport; do
fail "$artifact must only be an input of the Solr 9 bridge"
done
+for artifact in slf4j-api slf4j-jdk14; do
+ grep -E "name=\"$artifact\" rev=\"1.7.36\" conf=\"solr9-bridge->master\"" ivy.xml >/dev/null 2>&1 || \
+ fail "$artifact 1.7.36 must only be an input of the Solr 9 bridge"
+done
+
+grep -E 'org="org.eclipse.jetty.toolchain" name="jetty-servlet-api" rev="4.0.9"' ivy.xml >/dev/null 2>&1 || \
+ fail "Jetty's EE8 Servlet 4 API must be an explicit dependency"
+grep -E 'exclude org="javax.servlet" module="javax.servlet-api"' ivy.xml >/dev/null 2>&1 || \
+ fail "transitive javax.servlet-api artifacts must be excluded"
+
+expected_jetty_version=$(sed -n \
+ 's/.*org="org.eclipse.jetty" name="jetty-server" rev="\([^"]*\)".*/\1/p' \
+ ivy.xml)
+[ -n "$expected_jetty_version" ] || \
+ fail "could not determine the public Jetty version from jetty-server in ivy.xml"
+[ "$(printf '%s\n' "$expected_jetty_version" | wc -l | tr -d ' ')" -eq 1 ] || \
+ fail "jetty-server must declare exactly one public Jetty version"
+
if [ -d lib ]; then
+ public_jetty_count=0
+ for artifact in lib/jetty-*.jar; do
+ [ -e "$artifact" ] || continue
+ case $(basename "$artifact") in
+ jetty-servlet-api-*.jar)
+ # Jetty's Servlet 4 toolchain has its own version line.
+ continue
+ ;;
+ esac
+ public_jetty_count=$((public_jetty_count + 1))
+ case $(basename "$artifact") in
+ *-"$expected_jetty_version".jar) ;;
+ *) fail "public Jetty artifact is not on version $expected_jetty_version: $artifact" ;;
+ esac
+ done
+ [ "$public_jetty_count" -gt 0 ] || \
+ fail "no public Jetty $expected_jetty_version artifacts found"
+
+ case "$expected_jetty_version" in
+ 12.*)
+ for artifact in lib/jetty-continuation-*.jar; do
+ [ -e "$artifact" ] || continue
+ fail "Jetty 9-only artifact remains on the Jetty 12 classpath: $artifact"
+ done
+ ;;
+ esac
+
+ servlet_api_count=0
+ for artifact in lib/*servlet-api-*.jar; do
+ [ -e "$artifact" ] || continue
+ servlet_api_count=$((servlet_api_count + 1))
+ [ "$(basename "$artifact")" = "jetty-servlet-api-4.0.9.jar" ] || \
+ fail "unexpected Servlet API artifact: $artifact"
+ done
+ [ "$servlet_api_count" -eq 1 ] || \
+ fail "expected exactly one public Servlet API artifact, found $servlet_api_count"
+ jar tf lib/jetty-servlet-api-4.0.9.jar | grep '^javax/servlet/resources/web-app_4_0.xsd$' >/dev/null 2>&1 || \
+ fail "Jetty Servlet API is missing the EE8 web.xml schema"
+
for pattern in \
'jetty-deploy-*.jar' \
'jetty-jmx-*.jar' \
'solr-core-*.jar' \
'solr-solrj-*.jar' \
'solr-scripting-*.jar' \
- 'http2-*.jar'; do
+ 'http2-*.jar' \
+ 'slf4j-*-1.7.36.jar'; do
for artifact in lib/$pattern; do
[ -e "$artifact" ] || continue
fail "forbidden resolved artifact: $artifact"
@@ -66,7 +124,9 @@ if [ -d lib ]; then
jetty-util-9.4.58.v20250814 \
http2-client-9.4.58.v20250814 \
http2-common-9.4.58.v20250814 \
- http2-http-client-transport-9.4.58.v20250814; do
+ http2-http-client-transport-9.4.58.v20250814 \
+ slf4j-api-1.7.36 \
+ slf4j-jdk14-1.7.36; do
jar="lib/solr9-bridge-$artifact.jar"
[ -f "$jar" ] || fail "missing generated bridge artifact: $jar"
if jar tf "$jar" | grep '^org/eclipse/jetty/' >/dev/null 2>&1; then
@@ -75,7 +135,13 @@ if [ -d lib ]; then
if zipgrep -a -E 'org(/|\.)eclipse(/|\.)jetty' "$jar" >/dev/null 2>&1; then
fail "unrelocated Jetty reference in $jar"
fi
+ if jar tf "$jar" | grep '^org/slf4j/' >/dev/null 2>&1; then
+ fail "unrelocated SLF4J class in $jar"
+ fi
+ if zipgrep -a -E 'org(/|\.)slf4j' "$jar" >/dev/null 2>&1; then
+ fail "unrelocated SLF4J reference in $jar"
+ fi
done
fi
-echo "PASS: Solr 9 uses only the relocated Jetty client island."
+echo "PASS: Solr 9 uses only the relocated Jetty client and SLF4J 1.7 island."
diff --git a/test/jetty/RelocateJettyPackages.java b/test/jetty/RelocateJettyPackages.java
index 0766388e5..01939f5d2 100644
--- a/test/jetty/RelocateJettyPackages.java
+++ b/test/jetty/RelocateJettyPackages.java
@@ -1,9 +1,10 @@
/*
- * Proof-of-concept bytecode relocator for the Jetty 12 migration.
+ * Build-time bytecode relocator for the Jetty 12 migration.
*
* This is deliberately a small build tool, not YaCy runtime code. It keeps
* Solr's public packages unchanged while moving its private Jetty 9 linkage
- * below net.yacy.solr9.jetty.
+ * below net.yacy.solr9.jetty and its SLF4J 1.7 linkage below
+ * net.yacy.solr9.slf4j.
*/
package net.yacy.test.jetty;
@@ -26,10 +27,12 @@ import org.objectweb.asm.commons.Remapper;
public final class RelocateJettyPackages {
- private static final String SOURCE_INTERNAL = "org/eclipse/jetty";
- private static final String TARGET_INTERNAL = "net/yacy/solr9/jetty";
- private static final String SOURCE_BINARY = "org.eclipse.jetty";
- private static final String TARGET_BINARY = "net.yacy.solr9.jetty";
+ private static final Relocation[] RELOCATIONS = {
+ new Relocation("org/eclipse/jetty", "net/yacy/solr9/jetty",
+ "org.eclipse.jetty", "net.yacy.solr9.jetty"),
+ new Relocation("org/slf4j", "net/yacy/solr9/slf4j",
+ "org.slf4j", "net.yacy.solr9.slf4j")
+ };
private RelocateJettyPackages() {
}
@@ -43,7 +46,7 @@ public final class RelocateJettyPackages {
private static void relocate(final Path input, final Path output) throws IOException {
Files.createDirectories(output.toAbsolutePath().getParent());
- final Remapper remapper = new JettyRemapper();
+ final Remapper remapper = new BridgeRemapper();
try (JarFile source = new JarFile(input.toFile());
JarOutputStream target = new JarOutputStream(Files.newOutputStream(output))) {
@@ -62,8 +65,7 @@ public final class RelocateJettyPackages {
if (entry.getName().endsWith(".class")) {
target.write(relocateClass(content, remapper));
} else if (entry.getName().startsWith("META-INF/services/")) {
- target.write(new String(content, StandardCharsets.UTF_8)
- .replace(SOURCE_BINARY, TARGET_BINARY)
+ target.write(relocateText(new String(content, StandardCharsets.UTF_8))
.getBytes(StandardCharsets.UTF_8));
} else {
target.write(content);
@@ -74,28 +76,42 @@ public final class RelocateJettyPackages {
}
}
- private static final class JettyRemapper extends Remapper {
+ private static final class BridgeRemapper extends Remapper {
- private JettyRemapper() {
+ private BridgeRemapper() {
super(Opcodes.ASM9);
}
@Override
public String map(final String internalName) {
- return replacePrefix(internalName, SOURCE_INTERNAL, TARGET_INTERNAL);
+ return relocateInternalName(internalName);
}
@Override
public Object mapValue(final Object value) {
if (value instanceof String) {
final String text = (String) value;
- return text.replace(SOURCE_BINARY, TARGET_BINARY)
- .replace(SOURCE_INTERNAL, TARGET_INTERNAL);
+ return relocateText(text);
}
return super.mapValue(value);
}
}
+ private static final class Relocation {
+ private final String sourceInternal;
+ private final String targetInternal;
+ private final String sourceBinary;
+ private final String targetBinary;
+
+ private Relocation(final String sourceInternal, final String targetInternal,
+ final String sourceBinary, final String targetBinary) {
+ this.sourceInternal = sourceInternal;
+ this.targetInternal = targetInternal;
+ this.sourceBinary = sourceBinary;
+ this.targetBinary = targetBinary;
+ }
+ }
+
private static byte[] relocateClass(final byte[] content, final Remapper remapper) {
final ClassReader reader = new ClassReader(content);
final ClassWriter writer = new ClassWriter(0);
@@ -120,13 +136,30 @@ public final class RelocateJettyPackages {
}
private static String relocateEntryName(final String name) {
- final String internalName = replacePrefix(name, SOURCE_INTERNAL, TARGET_INTERNAL);
+ final String internalName = relocateInternalName(name);
if (internalName.startsWith("META-INF/services/")) {
- return internalName.replace(SOURCE_BINARY, TARGET_BINARY);
+ return relocateText(internalName);
}
return internalName;
}
+ private static String relocateInternalName(final String name) {
+ String relocated = name;
+ for (final Relocation relocation : RELOCATIONS) {
+ relocated = replacePrefix(relocated, relocation.sourceInternal, relocation.targetInternal);
+ }
+ return relocated;
+ }
+
+ private static String relocateText(final String text) {
+ String relocated = text;
+ for (final Relocation relocation : RELOCATIONS) {
+ relocated = relocated.replace(relocation.sourceBinary, relocation.targetBinary)
+ .replace(relocation.sourceInternal, relocation.targetInternal);
+ }
+ return relocated;
+ }
+
private static byte[] readAll(final InputStream stream) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
stream.transferTo(output);
diff --git a/test/proxy-smoke-test.sh b/test/proxy-smoke-test.sh
new file mode 100755
index 000000000..ccae2227b
--- /dev/null
+++ b/test/proxy-smoke-test.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env sh
+
+# Live HTTP proxy and CONNECT contract. The peer must be started with the
+# transparent proxy enabled and both targets must be controlled test services.
+
+set -eu
+
+proxy=${YACY_SMOKE_PROXY:-}
+http_target=${YACY_SMOKE_PROXY_HTTP_TARGET:-}
+https_target=${YACY_SMOKE_PROXY_HTTPS_TARGET:-}
+[ -n "$proxy" ] && [ -n "$http_target" ] && [ -n "$https_target" ] || {
+ echo "SKIP: set YACY_SMOKE_PROXY, YACY_SMOKE_PROXY_HTTP_TARGET and YACY_SMOKE_PROXY_HTTPS_TARGET" >&2
+ exit 2
+}
+
+timeout=${YACY_SMOKE_TIMEOUT:-30}
+curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \
+ --noproxy '' \
+ "$http_target" >/dev/null
+echo "ok 1 - HTTP proxy traffic"
+
+# An HTTPS request through an HTTP proxy necessarily establishes a CONNECT tunnel.
+curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \
+ --noproxy '' \
+ "$https_target" >/dev/null
+echo "ok 2 - HTTPS CONNECT tunnel"
+
+echo "PASS: proxy and CONNECT contract"
diff --git a/test/remote-solr-smoke-test.sh b/test/remote-solr-smoke-test.sh
new file mode 100755
index 000000000..84bea117a
--- /dev/null
+++ b/test/remote-solr-smoke-test.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env sh
+
+# Exercises YaCy's Apache-HttpClient-backed RemoteInstance against an external
+# Solr collection. This is an opt-in integration gate.
+
+set -eu
+
+[ -n "${YACY_REMOTE_SOLR_URL:-}" ] || {
+ echo "SKIP: set YACY_REMOTE_SOLR_URL (for example http://host:8983/solr/)" >&2
+ exit 2
+}
+
+repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
+cd "$repo_root"
+[ -d build/classes/java/main ] || { echo "FAIL: run ant compile first" >&2; exit 1; }
+
+work=$(mktemp -d "${TMPDIR:-/tmp}/yacy-remote-solr-smoke.XXXXXX")
+trap 'rm -rf "$work"' EXIT HUP INT TERM
+classpath=build/classes/java/main
+for archive in lib/*.jar; do
+ case $(basename "$archive") in yacycore.jar) continue ;; esac
+ classpath="$classpath:$archive"
+done
+
+javac --release 17 -cp "$classpath" -d "$work" \
+ test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java
+java -cp "$work:$classpath" net.yacy.cora.federate.solr.connector.RemoteSolrSmoke \
+ "$YACY_REMOTE_SOLR_URL"
+
+echo "PASS: remote Solr request through YaCy RemoteInstance"