From 7a9fa7094ef4deb0e25029ca08d48ba0f5e589d5 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sat, 12 Sep 2026 13:47:32 -0400 Subject: [PATCH 1/5] test(observability): re-enable OtlpExportIntegrationTest The class had been @Disabled since #41 because starting the Grafana LGTM Testcontainer threw a ClassNotFoundException for org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP. The Jetty pin has since moved with the SolrJ 10 bump, jetty-client 12.0.34 is on the test runtime classpath and contains that class, and the container now starts cleanly. With the annotation removed, three methods failed with AuthenticationCredentialsNotFoundException: the test calls @PreAuthorize service methods directly, with no MCP request and hence no principal. Rather than switch http.security.enabled off, the test authenticates its own thread with a TestingAuthenticationToken in @BeforeEach and clears it in @AfterEach, so method security stays active while OTLP export is verified. All seven methods pass; the build no longer reports any skipped tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019zJ9WA8nNyA5Yxb8ueM7vV Signed-off-by: Aditya Parikh --- .../OtlpExportIntegrationTest.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java index 2c83aae4..5aa5c273 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java @@ -27,14 +27,17 @@ import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.search.SearchService; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; import org.testcontainers.grafana.LgtmStackContainer; import org.testcontainers.junit.jupiter.Container; @@ -73,7 +76,6 @@ * functionality is tested by {@link DistributedTracingTest} which uses * SimpleTracer and passes all tests successfully. */ -@Disabled("Jetty HTTP client ClassNotFoundException with LgtmStackContainer - see class javadoc") @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { @@ -118,6 +120,24 @@ static void setUpCollection(@Autowired SolrClient solrClient) throws Exception { createRequest.process(solrClient); } + /** + * The http profile keeps secure-by-default on, so the {@code @PreAuthorize} + * service methods this test calls directly need an authenticated principal. + * Over MCP the OAuth2 filter chain supplies one; here the test thread does, + * which keeps method security active rather than switching it off. + */ + @BeforeEach + void authenticateTestThread() { + var authentication = new TestingAuthenticationToken("otlp-test", null, "ROLE_USER"); + authentication.setAuthenticated(true); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @AfterEach + void clearAuthentication() { + SecurityContextHolder.clearContext(); + } + @Test void shouldExportTracesWithoutErrors() throws Exception { // Given: Some test data From 0c9576dd69e0358e729e3eb10491d46262e1daa9 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sat, 12 Sep 2026 17:28:56 -0400 Subject: [PATCH 2/5] build(test): pin Testcontainers images in the version catalog Pin the Solr and Grafana LGTM images the tests start to exact tags, solr:9.9.0-slim and grafana/otel-lgtm:0.33.0, declared once as test-image-solr / test-image-lgtm in gradle/libs.versions.toml. Previously the Solr default was spelled out three times and the LGTM image floated on latest: the locally cached "latest" was 0.28.0 from May while Docker Hub's had moved to 0.33.0, so the same test run meant different things on different machines. processTestResources expands the pins into test-images.properties and the Gradle test tasks also forward them as system properties. A new TestImages resolver prefers a non-blank -Dsolr.test.image / -Dlgtm.test.image, so the Solr compatibility matrix keeps working, and falls back to the resource otherwise; the resource is included in the native test binary explicitly. TestImagesTest fails the build if a pin is missing, floats on latest, or is not an exact release tag. Verified: ./gradlew build 407 tests, 0 failures, 0 skipped; ./gradlew nativeTest -Pnative 405 tests, 0 failures, 138 skipped (Mockito). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019zJ9WA8nNyA5Yxb8ueM7vV Signed-off-by: Aditya Parikh --- AGENTS.md | 4 +- build.gradle.kts | 41 ++++++++- dev-docs/DEVELOPMENT.md | 6 +- gradle/libs.versions.toml | 6 ++ .../apache/solr/mcp/server/TestImages.java | 84 +++++++++++++++++++ .../solr/mcp/server/TestImagesTest.java | 81 ++++++++++++++++++ .../server/TestcontainersConfiguration.java | 3 +- .../OtlpExportIntegrationTest.java | 3 +- src/test/resources/test-images.properties | 20 +++++ 9 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/apache/solr/mcp/server/TestImages.java create mode 100644 src/test/java/org/apache/solr/mcp/server/TestImagesTest.java create mode 100644 src/test/resources/test-images.properties diff --git a/AGENTS.md b/AGENTS.md index 811ce1b7..2e5a1847 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -370,12 +370,12 @@ are exercised. The Jib JVM path runs in `build-and-publish.yml`. ### Solr Version Compatibility Testing -The Solr Docker image used in tests is configurable via the `solr.test.image` system property (default: `solr:9.9-slim`): +The Solr Docker image used in tests is pinned as `test-image-solr` in `gradle/libs.versions.toml` (the Grafana LGTM image for the OTLP test as `test-image-lgtm`). `TestImagesTest` fails the build if a pin is missing, floats on `latest`, or is not an exact release tag. Override the Solr image for one run with the `solr.test.image` system property: ```bash ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 ./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4 -./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (default) +./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (the pinned default is 9.9.0-slim) ./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10 ./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10 ``` diff --git a/build.gradle.kts b/build.gradle.kts index 094209ed..40b7d596 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -204,6 +204,36 @@ springBoot { buildInfo() } +// Testcontainers image pins live in gradle/libs.versions.toml; -Dsolr.test.image +// and -Dlgtm.test.image override them for a single run (e.g. the Solr +// compatibility matrix in CI). +val solrTestImage = + System.getProperty( + "solr.test.image", + libs.versions.test.image.solr + .get(), + ) +val lgtmTestImage = + System.getProperty( + "lgtm.test.image", + libs.versions.test.image.lgtm + .get(), + ) + +tasks.processTestResources { + val solrPin = + libs.versions.test.image.solr + .get() + val lgtmPin = + libs.versions.test.image.lgtm + .get() + inputs.property("solrTestImage", solrPin) + inputs.property("lgtmTestImage", lgtmPin) + filesMatching("test-images.properties") { + expand("solrTestImage" to solrPin, "lgtmTestImage" to lgtmPin) + } +} + tasks.withType { useJUnitPlatform { // Only exclude docker integration tests from regular test runs, not from dockerIntegrationTest @@ -218,8 +248,9 @@ tasks.withType { if (name != "dockerIntegrationTest") { dependsOn(tasks.bootJar) } - // Forward solr.test.image system property to test JVMs for Solr version compatibility testing - systemProperty("solr.test.image", System.getProperty("solr.test.image", "solr:9.9-slim")) + // Forward the Testcontainers image pins (or a per-run -D override) to test JVMs. + systemProperty("solr.test.image", solrTestImage) + systemProperty("lgtm.test.image", lgtmTestImage) if (name != "dockerIntegrationTest") { finalizedBy(tasks.jacocoTestReport) } @@ -255,7 +286,8 @@ tasks.register("integrationTest") { testClassesDirs = sourceSets["test"].output.classesDirs classpath = sourceSets["test"].runtimeClasspath - systemProperty("solr.test.image", System.getProperty("solr.test.image", "solr:9.9-slim")) + systemProperty("solr.test.image", solrTestImage) + systemProperty("lgtm.test.image", lgtmTestImage) mustRunAfter(tasks.named("unitTest")) finalizedBy(tasks.jacocoTestReport) @@ -550,6 +582,9 @@ if (nativeBuild) { "--initialize-at-build-time=org.junit.platform.launcher", "--initialize-at-build-time=org.junit.platform.engine", "--initialize-at-build-time=org.junit.jupiter.engine.descriptor", + // TestImages reads the Testcontainers image pins from this resource + // when no -D override is present (see gradle/libs.versions.toml). + "-H:IncludeResources=test-images\\.properties", ) } } diff --git a/dev-docs/DEVELOPMENT.md b/dev-docs/DEVELOPMENT.md index c11def43..fe2f252a 100644 --- a/dev-docs/DEVELOPMENT.md +++ b/dev-docs/DEVELOPMENT.md @@ -189,12 +189,12 @@ This runs tests tagged with `@Tag("docker-integration")` which verify: ### Solr Version Compatibility -Tests run against `solr:9.9-slim` by default. Point them at another Solr version with the `solr.test.image` system property: +Tests run against the Solr image pinned as `test-image-solr` in `gradle/libs.versions.toml` by default (the LGTM image for the OTLP test is pinned there too, as `test-image-lgtm`). Point them at another Solr version with the `solr.test.image` system property: ```bash ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 ./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4 -./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (default) +./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (the pinned default is 9.9.0-slim) ./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10 ./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10 ``` @@ -205,7 +205,7 @@ Tests run against `solr:9.9-slim` by default. Point them at another Solr version endpoint was removed in Solr 10, so `getCacheMetrics()`/`getHandlerMetrics()` catch `RuntimeException` and return `null` — `cacheStats`/`handlerStats` from `get-collection-stats` are therefore always `null` on Solr 10 (a future migration to `/admin/metrics` will restore -them). SolrJ is on 10.0.0; since `solr.test.image` defaults to `solr:9.9-slim`, the standard +them). SolrJ is on 10.0.0; since the pinned test image is Solr 9.9, the standard build runs a SolrJ 10 client against a Solr 9.9 server. ### Test with MCP Inspector diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4129317..d5e951ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,12 @@ nullaway = "0.13.1" # Test dependencies testcontainers = "1.21.3" +# Docker images started by Testcontainers. Exact tags only (never "latest" or a +# moving minor): TestImagesTest fails the build otherwise. Forwarded to tests by +# processTestResources (test-images.properties) and as system properties; the +# solr.test.image / lgtm.test.image system properties override them per run. +test-image-solr = "solr:9.9.0-slim" +test-image-lgtm = "grafana/otel-lgtm:0.33.0" awaitility = "4.3.0" opentelemetry-instrumentation-bom = "2.26.1" diff --git a/src/test/java/org/apache/solr/mcp/server/TestImages.java b/src/test/java/org/apache/solr/mcp/server/TestImages.java new file mode 100644 index 00000000..ddd1487d --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/TestImages.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Properties; + +/** + * Resolves the Docker images the Testcontainers-based tests start. + * + *

+ * The pins live in {@code gradle/libs.versions.toml}; + * {@code processTestResources} expands them into + * {@code test-images.properties}, and the Gradle test tasks also forward them + * as system properties. A non-blank system property wins, so + * {@code ./gradlew test -Dsolr.test.image=solr:9.4-slim} still drives the Solr + * compatibility matrix. The resource fallback keeps class initialisers such as + * {@code @Container static} fields working wherever the properties are not + * forwarded, e.g. under {@code processTestAot}. + */ +public final class TestImages { + + public static final String SOLR_PROPERTY = "solr.test.image"; + public static final String LGTM_PROPERTY = "lgtm.test.image"; + + private static final String RESOURCE = "/test-images.properties"; + + private TestImages() { + } + + /** @return the Solr image to start, e.g. {@code solr:9.9.0-slim} */ + public static String solr() { + return resolve(SOLR_PROPERTY); + } + + /** + * @return the Grafana LGTM image to start, e.g. + * {@code grafana/otel-lgtm:0.33.0} + */ + public static String lgtm() { + return resolve(LGTM_PROPERTY); + } + + private static String resolve(String key) { + String override = System.getProperty(key); + if (override != null && !override.isBlank()) { + return override.trim(); + } + String pinned = pins().getProperty(key, "").trim(); + if (pinned.isBlank() || pinned.startsWith("${")) { + throw new IllegalStateException(key + " is not set. Run the tests through Gradle so " + RESOURCE + + " is expanded from gradle/libs.versions.toml, or pass -D" + key + "=."); + } + return pinned; + } + + private static Properties pins() { + Properties properties = new Properties(); + try (InputStream in = TestImages.class.getResourceAsStream(RESOURCE)) { + if (in != null) { + properties.load(in); + } + } catch (IOException e) { + throw new UncheckedIOException("Cannot read " + RESOURCE, e); + } + return properties; + } +} diff --git a/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java b/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java new file mode 100644 index 00000000..7110335f --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.utility.DockerImageName; + +/** + * Guards the Testcontainers image pins. Every image the tests start must carry + * an explicit, non-floating tag so a run is reproducible and an image bump is a + * reviewed change to {@code gradle/libs.versions.toml}, never a silent pull of + * whatever {@code latest} means today. + */ +class TestImagesTest { + + @AfterEach + void clearOverrides() { + System.clearProperty(TestImages.SOLR_PROPERTY); + System.clearProperty(TestImages.LGTM_PROPERTY); + } + + @Test + void solrImageIsPinnedToAnExactTag() { + System.clearProperty(TestImages.SOLR_PROPERTY); + + DockerImageName image = DockerImageName.parse(TestImages.solr()); + + assertEquals("solr", image.getRepository()); + assertPinned(image); + } + + @Test + void lgtmImageIsPinnedToAnExactTag() { + System.clearProperty(TestImages.LGTM_PROPERTY); + + DockerImageName image = DockerImageName.parse(TestImages.lgtm()); + + assertEquals("grafana/otel-lgtm", image.getRepository()); + assertPinned(image); + } + + @Test + void systemPropertyOverridesTheCatalogPin() { + System.setProperty(TestImages.SOLR_PROPERTY, "solr:8.11-slim"); + + assertEquals("solr:8.11-slim", TestImages.solr()); + } + + @Test + void blankSystemPropertyFallsBackToTheCatalogPin() { + System.setProperty(TestImages.SOLR_PROPERTY, " "); + + assertTrue(TestImages.solr().startsWith("solr:"), TestImages.solr()); + } + + private static void assertPinned(DockerImageName image) { + String tag = image.getVersionPart(); + assertFalse(tag.isBlank(), "image must carry an explicit tag: " + image); + assertFalse("latest".equals(tag), "image must not float on latest: " + image); + assertTrue(tag.matches("\\d+\\.\\d+\\.\\d+.*"), "tag must be an exact release, not a moving minor: " + image); + } +} diff --git a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java index 5f6267a2..0a53de8e 100644 --- a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java +++ b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java @@ -29,8 +29,7 @@ public class TestcontainersConfiguration { @Bean SolrContainer solr() { - String solrImage = System.getProperty("solr.test.image", "solr:9.9-slim"); - return new SolrContainer(DockerImageName.parse(solrImage)); + return new SolrContainer(DockerImageName.parse(TestImages.solr())); } @Bean diff --git a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java index 5aa5c273..7ec5aa23 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.mcp.server.TestImages; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.search.SearchService; @@ -98,7 +99,7 @@ class OtlpExportIntegrationTest { */ @Container @ServiceConnection - static LgtmStackContainer lgtmStack = new LgtmStackContainer("grafana/otel-lgtm:latest"); + static LgtmStackContainer lgtmStack = new LgtmStackContainer(TestImages.lgtm()); @Autowired private SearchService searchService; diff --git a/src/test/resources/test-images.properties b/src/test/resources/test-images.properties new file mode 100644 index 00000000..b1af1c9c --- /dev/null +++ b/src/test/resources/test-images.properties @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Expanded by processTestResources from gradle/libs.versions.toml. Change the +# pins there, not here. Read by org.apache.solr.mcp.server.TestImages. +solr.test.image=${solrTestImage} +lgtm.test.image=${lgtmTestImage} From dfab359aa4548e19594012c54d2a6351cd779ca7 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sat, 12 Sep 2026 23:03:53 -0400 Subject: [PATCH 3/5] build(test): read catalog pins directly in TestImagesTest TestImagesTest cleared the real solr.test.image / lgtm.test.image system properties, and Gradle runs every test class in one fork, so any container started after it fell back to the catalog pin. The Solr compatibility matrix in CI passes -Dsolr.test.image and could end up testing the pinned version while reporting another. The pin-guard tests now read the expanded resource through TestImages.pinned(key) and never touch system properties; the two override tests that covered three lines of resolve() are dropped. Also hoists the catalog pins to one declaration in build.gradle.kts, removes the duplicate systemProperty forwarding on integrationTest (withType already covers it), and stops the docs from demonstrating the moving solr:9.9-slim tag as an override example. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh --- AGENTS.md | 4 +-- build.gradle.kts | 29 +++++-------------- dev-docs/DEVELOPMENT.md | 2 +- .../apache/solr/mcp/server/TestImages.java | 5 ++++ .../solr/mcp/server/TestImagesTest.java | 29 ++----------------- 5 files changed, 18 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2e5a1847..c8c63318 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -375,7 +375,7 @@ The Solr Docker image used in tests is pinned as `test-image-solr` in `gradle/li ```bash ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 ./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4 -./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (the pinned default is 9.9.0-slim) +./gradlew test -Dsolr.test.image=solr:9.9.0-slim # Solr 9.9 (the pinned default) ./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10 ./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10 ``` @@ -393,7 +393,7 @@ Remaining known differences from Solr 9: - **`/admin/mbeans` removed:** Cache and handler stats from `getCollectionStats()` will always be `null` on Solr 10. A future migration to `/admin/metrics` will restore these metrics. - **Metrics migration:** Dropwizard metrics replaced by OpenTelemetry. Metric names switch to snake_case in Solr 10. - **SolrJ base URL:** Already uses root URLs — **no change needed**. -- **SolrJ version:** `solr-solrj` is on 10.0.0 (`gradle/libs.versions.toml`), released to Maven Central and bumped in #58. Jetty artifacts are declared versionless and managed by Spring Boot's BOM, so there is no separate Jetty pin to update. Note the client is *newer* than the default test server: `solr.test.image` defaults to `solr:9.9-slim`, so the standard build exercises a SolrJ 10 client against Solr 9.9. +- **SolrJ version:** `solr-solrj` is on 10.0.0 (`gradle/libs.versions.toml`), released to Maven Central and bumped in #58. Jetty artifacts are declared versionless and managed by Spring Boot's BOM, so there is no separate Jetty pin to update. Note the client is *newer* than the default test server: `solr.test.image` defaults to `solr:9.9.0-slim`, so the standard build exercises a SolrJ 10 client against Solr 9.9. ## Key Configuration diff --git a/build.gradle.kts b/build.gradle.kts index 40b7d596..7119de17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -207,26 +207,16 @@ springBoot { // Testcontainers image pins live in gradle/libs.versions.toml; -Dsolr.test.image // and -Dlgtm.test.image override them for a single run (e.g. the Solr // compatibility matrix in CI). -val solrTestImage = - System.getProperty( - "solr.test.image", - libs.versions.test.image.solr - .get(), - ) -val lgtmTestImage = - System.getProperty( - "lgtm.test.image", - libs.versions.test.image.lgtm - .get(), - ) +val solrPin = + libs.versions.test.image.solr + .get() +val lgtmPin = + libs.versions.test.image.lgtm + .get() +val solrTestImage = System.getProperty("solr.test.image", solrPin) +val lgtmTestImage = System.getProperty("lgtm.test.image", lgtmPin) tasks.processTestResources { - val solrPin = - libs.versions.test.image.solr - .get() - val lgtmPin = - libs.versions.test.image.lgtm - .get() inputs.property("solrTestImage", solrPin) inputs.property("lgtmTestImage", lgtmPin) filesMatching("test-images.properties") { @@ -286,9 +276,6 @@ tasks.register("integrationTest") { testClassesDirs = sourceSets["test"].output.classesDirs classpath = sourceSets["test"].runtimeClasspath - systemProperty("solr.test.image", solrTestImage) - systemProperty("lgtm.test.image", lgtmTestImage) - mustRunAfter(tasks.named("unitTest")) finalizedBy(tasks.jacocoTestReport) diff --git a/dev-docs/DEVELOPMENT.md b/dev-docs/DEVELOPMENT.md index fe2f252a..88b5afa6 100644 --- a/dev-docs/DEVELOPMENT.md +++ b/dev-docs/DEVELOPMENT.md @@ -194,7 +194,7 @@ Tests run against the Solr image pinned as `test-image-solr` in `gradle/libs.ver ```bash ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 ./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4 -./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (the pinned default is 9.9.0-slim) +./gradlew test -Dsolr.test.image=solr:9.9.0-slim # Solr 9.9 (the pinned default) ./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10 ./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10 ``` diff --git a/src/test/java/org/apache/solr/mcp/server/TestImages.java b/src/test/java/org/apache/solr/mcp/server/TestImages.java index ddd1487d..7af64de5 100644 --- a/src/test/java/org/apache/solr/mcp/server/TestImages.java +++ b/src/test/java/org/apache/solr/mcp/server/TestImages.java @@ -62,6 +62,11 @@ private static String resolve(String key) { if (override != null && !override.isBlank()) { return override.trim(); } + return pinned(key); + } + + /** The catalog pin for {@code key}, ignoring any {@code -D} override. */ + static String pinned(String key) { String pinned = pins().getProperty(key, "").trim(); if (pinned.isBlank() || pinned.startsWith("${")) { throw new IllegalStateException(key + " is not set. Run the tests through Gradle so " + RESOURCE diff --git a/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java b/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java index 7110335f..b2428bb7 100644 --- a/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java +++ b/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; @@ -32,17 +31,9 @@ */ class TestImagesTest { - @AfterEach - void clearOverrides() { - System.clearProperty(TestImages.SOLR_PROPERTY); - System.clearProperty(TestImages.LGTM_PROPERTY); - } - @Test void solrImageIsPinnedToAnExactTag() { - System.clearProperty(TestImages.SOLR_PROPERTY); - - DockerImageName image = DockerImageName.parse(TestImages.solr()); + DockerImageName image = DockerImageName.parse(TestImages.pinned(TestImages.SOLR_PROPERTY)); assertEquals("solr", image.getRepository()); assertPinned(image); @@ -50,28 +41,12 @@ void solrImageIsPinnedToAnExactTag() { @Test void lgtmImageIsPinnedToAnExactTag() { - System.clearProperty(TestImages.LGTM_PROPERTY); - - DockerImageName image = DockerImageName.parse(TestImages.lgtm()); + DockerImageName image = DockerImageName.parse(TestImages.pinned(TestImages.LGTM_PROPERTY)); assertEquals("grafana/otel-lgtm", image.getRepository()); assertPinned(image); } - @Test - void systemPropertyOverridesTheCatalogPin() { - System.setProperty(TestImages.SOLR_PROPERTY, "solr:8.11-slim"); - - assertEquals("solr:8.11-slim", TestImages.solr()); - } - - @Test - void blankSystemPropertyFallsBackToTheCatalogPin() { - System.setProperty(TestImages.SOLR_PROPERTY, " "); - - assertTrue(TestImages.solr().startsWith("solr:"), TestImages.solr()); - } - private static void assertPinned(DockerImageName image) { String tag = image.getVersionPart(); assertFalse(tag.isBlank(), "image must carry an explicit tag: " + image); From 36618cfb86d32604d6c4ffc3310bc2c9c1951500 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sat, 12 Sep 2026 23:03:28 -0400 Subject: [PATCH 4/5] test(observability): drop stale disabled-note and redundant setAuthenticated The class javadoc still said the test was disabled for a Jetty ClassNotFoundException; that paragraph now describes the LGTM container and the self-authenticated test thread. TestingAuthenticationToken's authorities constructor already marks the token authenticated, so the explicit call goes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ Signed-off-by: Aditya Parikh --- .../observability/OtlpExportIntegrationTest.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java index 7ec5aa23..c94fa088 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java @@ -68,14 +68,9 @@ * container integration which auto-configures OTLP export endpoints. * *

- * NOTE: This test is currently disabled due to a Jetty HTTP client - * ClassNotFoundException when using LgtmStackContainer. The - * testcontainers-grafana module requires - * {@code org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP} which - * is not properly resolved with the current Jetty BOM configuration. This is a - * known issue and can be addressed separately. The core distributed tracing - * functionality is tested by {@link DistributedTracingTest} which uses - * SimpleTracer and passes all tests successfully. + * Runs against a real LGTM container and authenticates its own thread, because + * the http profile keeps method security on. {@link DistributedTracingTest} + * covers the same tracing paths in-memory without a container. */ @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -130,7 +125,6 @@ static void setUpCollection(@Autowired SolrClient solrClient) throws Exception { @BeforeEach void authenticateTestThread() { var authentication = new TestingAuthenticationToken("otlp-test", null, "ROLE_USER"); - authentication.setAuthenticated(true); SecurityContextHolder.getContext().setAuthentication(authentication); } From a108e8d1752f9881dfd271355b0b22f8918a0839 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Wed, 16 Sep 2026 11:41:11 -0400 Subject: [PATCH 5/5] build(test): back out TestImages indirection per review Eric Pugh asked to back this out: it adds indirection for little value, and hides the Docker image name that should be visible right at the Testcontainers call, matching the pattern apache/solr itself uses. TestcontainersConfiguration and OtlpExportIntegrationTest now read System.getProperty("solr.test.image"/"lgtm.test.image", ) directly, so the image is visible in the call and -Dsolr.test.image still drives the Solr compatibility matrix in CI. TestImages.java, TestImagesTest.java, and the test-images.properties resource (plus its native -H:IncludeResources hint) are gone with the class that used them. The gradle/libs.versions.toml pins stay as the single declared version, with a comment that the call-site literals must be bumped alongside them. Verified: ./gradlew build 403 tests, 0 failures, 0 skipped (JDK 25). Co-Authored-By: Claude Sonnet 5 Signed-off-by: Aditya Parikh --- AGENTS.md | 2 +- build.gradle.kts | 11 --- gradle/libs.versions.toml | 7 +- .../apache/solr/mcp/server/TestImages.java | 89 ------------------- .../solr/mcp/server/TestImagesTest.java | 56 ------------ .../server/TestcontainersConfiguration.java | 5 +- .../OtlpExportIntegrationTest.java | 6 +- src/test/resources/test-images.properties | 20 ----- 8 files changed, 13 insertions(+), 183 deletions(-) delete mode 100644 src/test/java/org/apache/solr/mcp/server/TestImages.java delete mode 100644 src/test/java/org/apache/solr/mcp/server/TestImagesTest.java delete mode 100644 src/test/resources/test-images.properties diff --git a/AGENTS.md b/AGENTS.md index c8c63318..456ea2ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -370,7 +370,7 @@ are exercised. The Jib JVM path runs in `build-and-publish.yml`. ### Solr Version Compatibility Testing -The Solr Docker image used in tests is pinned as `test-image-solr` in `gradle/libs.versions.toml` (the Grafana LGTM image for the OTLP test as `test-image-lgtm`). `TestImagesTest` fails the build if a pin is missing, floats on `latest`, or is not an exact release tag. Override the Solr image for one run with the `solr.test.image` system property: +The Solr Docker image used in tests is pinned as `test-image-solr` in `gradle/libs.versions.toml` (the Grafana LGTM image for the OTLP test as `test-image-lgtm`), matching the literal default in `TestcontainersConfiguration`/`OtlpExportIntegrationTest`. Override the Solr image for one run with the `solr.test.image` system property: ```bash ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 diff --git a/build.gradle.kts b/build.gradle.kts index 7119de17..831cb2c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -216,14 +216,6 @@ val lgtmPin = val solrTestImage = System.getProperty("solr.test.image", solrPin) val lgtmTestImage = System.getProperty("lgtm.test.image", lgtmPin) -tasks.processTestResources { - inputs.property("solrTestImage", solrPin) - inputs.property("lgtmTestImage", lgtmPin) - filesMatching("test-images.properties") { - expand("solrTestImage" to solrPin, "lgtmTestImage" to lgtmPin) - } -} - tasks.withType { useJUnitPlatform { // Only exclude docker integration tests from regular test runs, not from dockerIntegrationTest @@ -569,9 +561,6 @@ if (nativeBuild) { "--initialize-at-build-time=org.junit.platform.launcher", "--initialize-at-build-time=org.junit.platform.engine", "--initialize-at-build-time=org.junit.jupiter.engine.descriptor", - // TestImages reads the Testcontainers image pins from this resource - // when no -D override is present (see gradle/libs.versions.toml). - "-H:IncludeResources=test-images\\.properties", ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5e951ba..ecf4df57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,9 +40,10 @@ nullaway = "0.13.1" # Test dependencies testcontainers = "1.21.3" # Docker images started by Testcontainers. Exact tags only (never "latest" or a -# moving minor): TestImagesTest fails the build otherwise. Forwarded to tests by -# processTestResources (test-images.properties) and as system properties; the -# solr.test.image / lgtm.test.image system properties override them per run. +# moving minor tag). Forwarded to tests as the solr.test.image / lgtm.test.image +# system properties, which also override them for a single run; the literal +# default at each Testcontainers call site (TestcontainersConfiguration, +# OtlpExportIntegrationTest) must be bumped along with these pins. test-image-solr = "solr:9.9.0-slim" test-image-lgtm = "grafana/otel-lgtm:0.33.0" awaitility = "4.3.0" diff --git a/src/test/java/org/apache/solr/mcp/server/TestImages.java b/src/test/java/org/apache/solr/mcp/server/TestImages.java deleted file mode 100644 index 7af64de5..00000000 --- a/src/test/java/org/apache/solr/mcp/server/TestImages.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.mcp.server; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.util.Properties; - -/** - * Resolves the Docker images the Testcontainers-based tests start. - * - *

- * The pins live in {@code gradle/libs.versions.toml}; - * {@code processTestResources} expands them into - * {@code test-images.properties}, and the Gradle test tasks also forward them - * as system properties. A non-blank system property wins, so - * {@code ./gradlew test -Dsolr.test.image=solr:9.4-slim} still drives the Solr - * compatibility matrix. The resource fallback keeps class initialisers such as - * {@code @Container static} fields working wherever the properties are not - * forwarded, e.g. under {@code processTestAot}. - */ -public final class TestImages { - - public static final String SOLR_PROPERTY = "solr.test.image"; - public static final String LGTM_PROPERTY = "lgtm.test.image"; - - private static final String RESOURCE = "/test-images.properties"; - - private TestImages() { - } - - /** @return the Solr image to start, e.g. {@code solr:9.9.0-slim} */ - public static String solr() { - return resolve(SOLR_PROPERTY); - } - - /** - * @return the Grafana LGTM image to start, e.g. - * {@code grafana/otel-lgtm:0.33.0} - */ - public static String lgtm() { - return resolve(LGTM_PROPERTY); - } - - private static String resolve(String key) { - String override = System.getProperty(key); - if (override != null && !override.isBlank()) { - return override.trim(); - } - return pinned(key); - } - - /** The catalog pin for {@code key}, ignoring any {@code -D} override. */ - static String pinned(String key) { - String pinned = pins().getProperty(key, "").trim(); - if (pinned.isBlank() || pinned.startsWith("${")) { - throw new IllegalStateException(key + " is not set. Run the tests through Gradle so " + RESOURCE - + " is expanded from gradle/libs.versions.toml, or pass -D" + key + "=."); - } - return pinned; - } - - private static Properties pins() { - Properties properties = new Properties(); - try (InputStream in = TestImages.class.getResourceAsStream(RESOURCE)) { - if (in != null) { - properties.load(in); - } - } catch (IOException e) { - throw new UncheckedIOException("Cannot read " + RESOURCE, e); - } - return properties; - } -} diff --git a/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java b/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java deleted file mode 100644 index b2428bb7..00000000 --- a/src/test/java/org/apache/solr/mcp/server/TestImagesTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.mcp.server; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.testcontainers.utility.DockerImageName; - -/** - * Guards the Testcontainers image pins. Every image the tests start must carry - * an explicit, non-floating tag so a run is reproducible and an image bump is a - * reviewed change to {@code gradle/libs.versions.toml}, never a silent pull of - * whatever {@code latest} means today. - */ -class TestImagesTest { - - @Test - void solrImageIsPinnedToAnExactTag() { - DockerImageName image = DockerImageName.parse(TestImages.pinned(TestImages.SOLR_PROPERTY)); - - assertEquals("solr", image.getRepository()); - assertPinned(image); - } - - @Test - void lgtmImageIsPinnedToAnExactTag() { - DockerImageName image = DockerImageName.parse(TestImages.pinned(TestImages.LGTM_PROPERTY)); - - assertEquals("grafana/otel-lgtm", image.getRepository()); - assertPinned(image); - } - - private static void assertPinned(DockerImageName image) { - String tag = image.getVersionPart(); - assertFalse(tag.isBlank(), "image must carry an explicit tag: " + image); - assertFalse("latest".equals(tag), "image must not float on latest: " + image); - assertTrue(tag.matches("\\d+\\.\\d+\\.\\d+.*"), "tag must be an exact release, not a moving minor: " + image); - } -} diff --git a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java index 0a53de8e..ef324e1f 100644 --- a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java +++ b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java @@ -29,7 +29,10 @@ public class TestcontainersConfiguration { @Bean SolrContainer solr() { - return new SolrContainer(DockerImageName.parse(TestImages.solr())); + // Pinned in gradle/libs.versions.toml as test-image-solr; -Dsolr.test.image + // overrides it for a single run (e.g. the Solr compatibility matrix in CI). + String solrImage = System.getProperty("solr.test.image", "solr:9.9.0-slim"); + return new SolrContainer(DockerImageName.parse(solrImage)); } @Bean diff --git a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java index c94fa088..9d60ca7f 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java @@ -24,7 +24,6 @@ import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.mcp.server.TestImages; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.search.SearchService; @@ -92,9 +91,12 @@ class OtlpExportIntegrationTest { * The {@code @ServiceConnection} annotation enables Spring Boot to recognize * this container for service connection auto-configuration. */ + // Pinned in gradle/libs.versions.toml as test-image-lgtm; -Dlgtm.test.image + // overrides it for a single run. @Container @ServiceConnection - static LgtmStackContainer lgtmStack = new LgtmStackContainer(TestImages.lgtm()); + static LgtmStackContainer lgtmStack = new LgtmStackContainer( + System.getProperty("lgtm.test.image", "grafana/otel-lgtm:0.33.0")); @Autowired private SearchService searchService; diff --git a/src/test/resources/test-images.properties b/src/test/resources/test-images.properties deleted file mode 100644 index b1af1c9c..00000000 --- a/src/test/resources/test-images.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Expanded by processTestResources from gradle/libs.versions.toml. Change the -# pins there, not here. Read by org.apache.solr.mcp.server.TestImages. -solr.test.image=${solrTestImage} -lgtm.test.image=${lgtmTestImage}