From feb35974ceb65661b2cca57bd99a826dbdce81cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:25:42 +0200 Subject: [PATCH 01/30] Add Selenium-based test harness for the AngularJS Admin UI - New opt-in JettyConfig.enableAdminUi flag makes JettySolrRunner serve the Admin UI static files and LoadAdminUiServlet like production web.xml does - New test sourceSet in solr/webapp with AdminUiTestBase: starts a 2-node cloud cluster with the UI enabled and drives it with headless Chrome via Selenium WebDriver; tests skip cleanly when no Chrome binary is found - First test: AdminUiDashboardTest asserts the dashboard displays versions, JVM and system stats matching the /admin/info/system API - Selenium 4.47.0 test-only dependency with license bookkeeping --- gradle/libs.versions.toml | 3 + solr/licenses/auto-service-LICENSE-ASL.txt | 176 +++++++++++ solr/licenses/auto-service-NOTICE.txt | 2 + .../auto-service-annotations-1.1.1.jar.sha1 | 1 + solr/licenses/byte-buddy-1.18.11.jar.sha1 | 1 + solr/licenses/jspecify-1.0.1.jar.sha1 | 1 + .../opentelemetry-api-1.65.0.jar.sha1 | 1 + .../opentelemetry-common-1.65.0.jar.sha1 | 1 + .../opentelemetry-context-1.65.0.jar.sha1 | 1 + ...telemetry-exporter-logging-1.65.0.jar.sha1 | 1 + .../opentelemetry-sdk-1.65.0.jar.sha1 | 1 + .../opentelemetry-sdk-common-1.65.0.jar.sha1 | 1 + ...dk-extension-autoconfigure-1.65.0.jar.sha1 | 1 + ...xtension-autoconfigure-spi-1.65.0.jar.sha1 | 1 + .../opentelemetry-sdk-logs-1.65.0.jar.sha1 | 1 + .../opentelemetry-sdk-metrics-1.65.0.jar.sha1 | 1 + .../opentelemetry-sdk-trace-1.65.0.jar.sha1 | 1 + solr/licenses/selenium-LICENSE-ASL.txt | 176 +++++++++++ solr/licenses/selenium-NOTICE.txt | 2 + solr/licenses/selenium-api-4.47.0.jar.sha1 | 1 + .../selenium-chrome-driver-4.47.0.jar.sha1 | 1 + .../selenium-chromium-driver-4.47.0.jar.sha1 | 1 + solr/licenses/selenium-http-4.47.0.jar.sha1 | 1 + solr/licenses/selenium-json-4.47.0.jar.sha1 | 1 + .../licenses/selenium-manager-4.47.0.jar.sha1 | 1 + solr/licenses/selenium-os-4.47.0.jar.sha1 | 1 + .../selenium-remote-driver-4.47.0.jar.sha1 | 1 + .../licenses/selenium-support-4.47.0.jar.sha1 | 1 + .../org/apache/solr/embedded/JettyConfig.java | 17 +- .../apache/solr/embedded/JettySolrRunner.java | 38 ++- solr/webapp/build.gradle | 23 ++ solr/webapp/gradle.lockfile | 291 ++++++++++-------- .../solr/webapp/AdminUiDashboardTest.java | 61 ++++ .../apache/solr/webapp/AdminUiTestBase.java | 272 ++++++++++++++++ 34 files changed, 952 insertions(+), 132 deletions(-) create mode 100644 solr/licenses/auto-service-LICENSE-ASL.txt create mode 100644 solr/licenses/auto-service-NOTICE.txt create mode 100644 solr/licenses/auto-service-annotations-1.1.1.jar.sha1 create mode 100644 solr/licenses/byte-buddy-1.18.11.jar.sha1 create mode 100644 solr/licenses/jspecify-1.0.1.jar.sha1 create mode 100644 solr/licenses/opentelemetry-api-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-common-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-context-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 create mode 100644 solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 create mode 100644 solr/licenses/selenium-LICENSE-ASL.txt create mode 100644 solr/licenses/selenium-NOTICE.txt create mode 100644 solr/licenses/selenium-api-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-chrome-driver-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-chromium-driver-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-http-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-json-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-manager-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-os-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-remote-driver-4.47.0.jar.sha1 create mode 100644 solr/licenses/selenium-support-4.47.0.jar.sha1 create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiDashboardTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f82ce16a6cb..59c631130ad4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -187,6 +187,7 @@ owasp-dependencycheck = "13.0.0" perfmark = "0.27.0" prometheus-metrics = "1.8.0" quicktheories = "0.26" +selenium = "4.47.0" semver4j = "6.0.0" slf4j = "2.0.17" spatial4j = "0.8" @@ -512,6 +513,8 @@ perfmark-api = { module = "io.perfmark:perfmark-api", version.ref = "perfmark" } prometheus-metrics-expositionformats = { module = "io.prometheus:prometheus-metrics-exposition-formats", version.ref = "prometheus-metrics" } prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus-metrics" } quicktheories-quicktheories = { module = "org.quicktheories:quicktheories", version.ref = "quicktheories" } +selenium-chromedriver = { module = "org.seleniumhq.selenium:selenium-chrome-driver", version.ref = "selenium" } +selenium-support = { module = "org.seleniumhq.selenium:selenium-support", version.ref = "selenium" } semver4j-semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jcloverslf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref = "slf4j" } diff --git a/solr/licenses/auto-service-LICENSE-ASL.txt b/solr/licenses/auto-service-LICENSE-ASL.txt new file mode 100644 index 000000000000..d0381d6d04c7 --- /dev/null +++ b/solr/licenses/auto-service-LICENSE-ASL.txt @@ -0,0 +1,176 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/solr/licenses/auto-service-NOTICE.txt b/solr/licenses/auto-service-NOTICE.txt new file mode 100644 index 000000000000..e053b41122dc --- /dev/null +++ b/solr/licenses/auto-service-NOTICE.txt @@ -0,0 +1,2 @@ +AutoService +Copyright 2013 Google LLC diff --git a/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 b/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 new file mode 100644 index 000000000000..5d49902ad62e --- /dev/null +++ b/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 @@ -0,0 +1 @@ +da12a15cd058ba90a0ff55357fb521161af4736d diff --git a/solr/licenses/byte-buddy-1.18.11.jar.sha1 b/solr/licenses/byte-buddy-1.18.11.jar.sha1 new file mode 100644 index 000000000000..643df148bb78 --- /dev/null +++ b/solr/licenses/byte-buddy-1.18.11.jar.sha1 @@ -0,0 +1 @@ +8fcc3779ff85fae5164cd1b977798ca1af388e06 diff --git a/solr/licenses/jspecify-1.0.1.jar.sha1 b/solr/licenses/jspecify-1.0.1.jar.sha1 new file mode 100644 index 000000000000..b901c10940be --- /dev/null +++ b/solr/licenses/jspecify-1.0.1.jar.sha1 @@ -0,0 +1 @@ +3d60fd98eb8ade73004f4195c37b6317e02cf3d7 diff --git a/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 new file mode 100644 index 000000000000..a78665451042 --- /dev/null +++ b/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 @@ -0,0 +1 @@ +8b5df7f216b8b75da02f4f70daef71e70c91ff5e diff --git a/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 new file mode 100644 index 000000000000..abef04ed4c6d --- /dev/null +++ b/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 @@ -0,0 +1 @@ +843f221202a008c893e18dafed0a024ef9d25ac1 diff --git a/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 new file mode 100644 index 000000000000..963bf088700f --- /dev/null +++ b/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 @@ -0,0 +1 @@ +d62950109b08e183ee6e397394a2443bc6bdbe84 diff --git a/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 new file mode 100644 index 000000000000..7c21f4ad37c0 --- /dev/null +++ b/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 @@ -0,0 +1 @@ +f15158bde45aab36263dd000d7401e9ca1a97455 diff --git a/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 new file mode 100644 index 000000000000..36f68b3eb539 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 @@ -0,0 +1 @@ +0ec81a4855a64cd088f3aa0de5bf581717565838 diff --git a/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 new file mode 100644 index 000000000000..086b29c0fa5d --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 @@ -0,0 +1 @@ +7dbe712d9b9f51a48022f1cd358c0ed7e80f6a06 diff --git a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 new file mode 100644 index 000000000000..6eacefb05e36 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 @@ -0,0 +1 @@ +28ae444a769d8cade194ca5bbdd6924a56b1d513 diff --git a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 new file mode 100644 index 000000000000..86cb298b9cd8 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 @@ -0,0 +1 @@ +635051e4ba91ea38350e61ed58a39d16d573e977 diff --git a/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 new file mode 100644 index 000000000000..fa27a1b36c23 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 @@ -0,0 +1 @@ +9982196f3ef9531cb985ae951eb5eba952959793 diff --git a/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 new file mode 100644 index 000000000000..8fa8f2bd8552 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 @@ -0,0 +1 @@ +75298219ef305b42193cb40ff1e72e0cf8df8f4f diff --git a/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 new file mode 100644 index 000000000000..35c23f879151 --- /dev/null +++ b/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 @@ -0,0 +1 @@ +581326fd733b17cdd37e005575073e44a0a9b8ba diff --git a/solr/licenses/selenium-LICENSE-ASL.txt b/solr/licenses/selenium-LICENSE-ASL.txt new file mode 100644 index 000000000000..d0381d6d04c7 --- /dev/null +++ b/solr/licenses/selenium-LICENSE-ASL.txt @@ -0,0 +1,176 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/solr/licenses/selenium-NOTICE.txt b/solr/licenses/selenium-NOTICE.txt new file mode 100644 index 000000000000..146d948e632b --- /dev/null +++ b/solr/licenses/selenium-NOTICE.txt @@ -0,0 +1,2 @@ +Copyright 2011-2025 Software Freedom Conservancy +Copyright 2004-2011 Selenium committers diff --git a/solr/licenses/selenium-api-4.47.0.jar.sha1 b/solr/licenses/selenium-api-4.47.0.jar.sha1 new file mode 100644 index 000000000000..a9dad164a08e --- /dev/null +++ b/solr/licenses/selenium-api-4.47.0.jar.sha1 @@ -0,0 +1 @@ +2c7f4b4d126fe8b6d8d7051179d7169b2c08a267 diff --git a/solr/licenses/selenium-chrome-driver-4.47.0.jar.sha1 b/solr/licenses/selenium-chrome-driver-4.47.0.jar.sha1 new file mode 100644 index 000000000000..9a548e4ab025 --- /dev/null +++ b/solr/licenses/selenium-chrome-driver-4.47.0.jar.sha1 @@ -0,0 +1 @@ +f88771402b909b60596bf7ace95b9f95747dc9b4 diff --git a/solr/licenses/selenium-chromium-driver-4.47.0.jar.sha1 b/solr/licenses/selenium-chromium-driver-4.47.0.jar.sha1 new file mode 100644 index 000000000000..fc6355ca26bf --- /dev/null +++ b/solr/licenses/selenium-chromium-driver-4.47.0.jar.sha1 @@ -0,0 +1 @@ +a47e90b53289e484d7ecc4131106fa3a05bde93b diff --git a/solr/licenses/selenium-http-4.47.0.jar.sha1 b/solr/licenses/selenium-http-4.47.0.jar.sha1 new file mode 100644 index 000000000000..4a0b5e66b823 --- /dev/null +++ b/solr/licenses/selenium-http-4.47.0.jar.sha1 @@ -0,0 +1 @@ +1195ab7dc6d47fcb03f9d03bee02eba52c9bfe46 diff --git a/solr/licenses/selenium-json-4.47.0.jar.sha1 b/solr/licenses/selenium-json-4.47.0.jar.sha1 new file mode 100644 index 000000000000..a0771021c272 --- /dev/null +++ b/solr/licenses/selenium-json-4.47.0.jar.sha1 @@ -0,0 +1 @@ +d784a24fb2d3acd6514550b02cb5a102ba856dbd diff --git a/solr/licenses/selenium-manager-4.47.0.jar.sha1 b/solr/licenses/selenium-manager-4.47.0.jar.sha1 new file mode 100644 index 000000000000..06eb8e8bf913 --- /dev/null +++ b/solr/licenses/selenium-manager-4.47.0.jar.sha1 @@ -0,0 +1 @@ +1dd28f2d82374c212023584aaa2e006c69104d1b diff --git a/solr/licenses/selenium-os-4.47.0.jar.sha1 b/solr/licenses/selenium-os-4.47.0.jar.sha1 new file mode 100644 index 000000000000..b092eed9e80e --- /dev/null +++ b/solr/licenses/selenium-os-4.47.0.jar.sha1 @@ -0,0 +1 @@ +d6edd474f2dac25168382cf68322b116593f8ade diff --git a/solr/licenses/selenium-remote-driver-4.47.0.jar.sha1 b/solr/licenses/selenium-remote-driver-4.47.0.jar.sha1 new file mode 100644 index 000000000000..843a3ffbd8f5 --- /dev/null +++ b/solr/licenses/selenium-remote-driver-4.47.0.jar.sha1 @@ -0,0 +1 @@ +8a3fa091e488c5a91e1581f6e38ce1c36fd3ec40 diff --git a/solr/licenses/selenium-support-4.47.0.jar.sha1 b/solr/licenses/selenium-support-4.47.0.jar.sha1 new file mode 100644 index 000000000000..dd9100813969 --- /dev/null +++ b/solr/licenses/selenium-support-4.47.0.jar.sha1 @@ -0,0 +1 @@ +5b182dce28b0f22ee7a3afa1a7966d6d83ef18f5 diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettyConfig.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettyConfig.java index 601c6b5f2b58..20516d7c007b 100644 --- a/solr/test-framework/src/java/org/apache/solr/embedded/JettyConfig.java +++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettyConfig.java @@ -37,6 +37,9 @@ public class JettyConfig { public final boolean enableV2; public final boolean enableGracefulShutdown; + /** If true, serve the Admin UI static files and index.html like the production web.xml does. */ + public final boolean enableAdminUi; + private JettyConfig( boolean onlyHttp1, int port, @@ -47,7 +50,8 @@ private JettyConfig( Map, String> extraFilters, SSLConfig sslConfig, boolean enableV2, - boolean enableGracefulShutdown) { + boolean enableGracefulShutdown, + boolean enableAdminUi) { this.onlyHttp1 = onlyHttp1; this.port = port; this.portRetryTime = portRetryTime; @@ -58,6 +62,7 @@ private JettyConfig( this.sslConfig = sslConfig; this.enableV2 = enableV2; this.enableGracefulShutdown = enableGracefulShutdown; + this.enableAdminUi = enableAdminUi; } public static Builder builder() { @@ -77,6 +82,7 @@ public static Builder builder(JettyConfig other) { builder.sslConfig = other.sslConfig; builder.enableV2 = other.enableV2; builder.enableGracefulShutdown = other.enableGracefulShutdown; + builder.enableAdminUi = other.enableAdminUi; return builder; } @@ -86,6 +92,7 @@ public static class Builder { int port = 0; boolean enableV2 = true; boolean enableGracefulShutdown = false; + boolean enableAdminUi = false; boolean stopAtShutdown = true; Long waitForLoadingCoresToFinishMs = 300000L; Map extraServlets = new TreeMap<>(); @@ -109,6 +116,11 @@ public Builder enableGracefulShutdown(boolean flag) { return this; } + public Builder enableAdminUi(boolean flag) { + this.enableAdminUi = flag; + return this; + } + public Builder setPort(int port) { this.port = port; return this; @@ -165,7 +177,8 @@ public JettyConfig build() { extraFilters, sslConfig, enableV2, - enableGracefulShutdown); + enableGracefulShutdown, + enableAdminUi); } } } diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java index b27651251f3d..0a0f8554da0c 100644 --- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java +++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java @@ -30,6 +30,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.EnumSet; import java.util.List; @@ -54,10 +56,12 @@ import org.apache.solr.metrics.SolrMetricManager; import org.apache.solr.servlet.AuthenticationFilter; import org.apache.solr.servlet.CoreContainerProvider; +import org.apache.solr.servlet.LoadAdminUiServlet; import org.apache.solr.servlet.RateLimitFilter; import org.apache.solr.servlet.RequiredSolrRequestFilter; import org.apache.solr.servlet.SolrServlet; import org.apache.solr.servlet.TracingFilter; +import org.apache.solr.util.ExternalPaths; import org.apache.solr.util.RestTestHarness; import org.apache.solr.util.SocketProxy; import org.apache.solr.util.TimeOut; @@ -65,6 +69,7 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.ee10.servlet.FilterHolder; import org.eclipse.jetty.ee10.servlet.FilterMapping; +import org.eclipse.jetty.ee10.servlet.ResourceServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.ee10.servlet.Source; @@ -293,7 +298,19 @@ private void init(int port) { final ServletContextHandler root = new ServletContextHandler("/solr", ServletContextHandler.NO_SESSIONS); root.setServer(server); - root.setBaseResource(ResourceFactory.of(server).newResource(".")); + if (config.enableAdminUi) { + Path webappDir = ExternalPaths.WEBAPP_HOME; + if (webappDir == null || !Files.exists(webappDir.resolve("index.html"))) { + throw new IllegalStateException( + "enableAdminUi requires the Admin UI webapp sources at /solr/webapp/web, " + + "but they could not be located (ExternalPaths.WEBAPP_HOME=" + + webappDir + + ")"); + } + root.setBaseResource(ResourceFactory.of(server).newResource(webappDir)); + } else { + root.setBaseResource(ResourceFactory.of(server).newResource(".")); + } root.addEventListener( // Install CCP first. Subclass CCP to do some pre-initialization new CoreContainerProvider() { @@ -327,6 +344,25 @@ public void contextInitialized(ServletContextEvent event) { // TODO: This needs to be driven by a parsing of web.xml eventually // though we still want to avoid classpath scanning. + if (config.enableAdminUi) { + // Serve the Admin UI like production web.xml does: static assets + LoadAdminUiServlet + ServletHolder staticHolder = root.getServletHandler().newServletHolder(Source.EMBEDDED); + staticHolder.setName("static"); + staticHolder.setHeldClass(ResourceServlet.class); + staticHolder.setInitParameter("pathInfoOnly", "false"); + staticHolder.setInitParameter("dirAllowed", "false"); + for (String pathSpec : + new String[] { + "/partials/*", "/libs/*", "/css/*", "/js/*", "/img/*", "/templates/*", "/ui/*" + }) { + root.addServlet(staticHolder, pathSpec); + } + ServletHolder adminUiHolder = root.getServletHandler().newServletHolder(Source.EMBEDDED); + adminUiHolder.setName("LoadAdminUI"); + adminUiHolder.setHeldClass(LoadAdminUiServlet.class); + root.addServlet(adminUiHolder, "/index.html"); + } + // This is our main workhorse - now a servlet instead of filter solrServlet = root.getServletHandler().newServletHolder(Source.EMBEDDED); solrServlet.setName("SolrServlet"); diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle index da2ba2cda085..71691b2260f6 100644 --- a/solr/webapp/build.gradle +++ b/solr/webapp/build.gradle @@ -22,6 +22,12 @@ plugins { description = 'Solr webapp' +ext { + // The Selenium-based Admin UI tests spawn external chromedriver/Chrome processes, + // which the security manager forbids + useSecurityManager = false +} + configurations { war {} serverLib @@ -43,6 +49,23 @@ dependencies { if (gradle.ext.withUiModule) { generatedUIBundle project(path: ":solr:ui", configuration: "wasmJsUIBundle") } + + // Browser-based tests of the AngularJS Admin UI (see src/test) + testImplementation project(':solr:core') + testImplementation project(':solr:solrj') + testImplementation project(':solr:test-framework') + testImplementation libs.carrotsearch.randomizedtesting.runner + testImplementation libs.junit.junit + testImplementation libs.selenium.chromedriver + testImplementation libs.selenium.support +} + +// Forward the browser-binary override for the Admin UI tests to the forked test JVM +tasks.withType(Test).configureEach { + def chromeBinary = providers.systemProperty('tests.ui.chrome.binary').orNull + if (chromeBinary != null) { + systemProperty 'tests.ui.chrome.binary', chromeBinary + } } war { diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile index a562330bd664..b655bafc1349 100644 --- a/solr/webapp/gradle.lockfile +++ b/solr/webapp/gradle.lockfile @@ -2,165 +2,198 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. # To regenerate this file, run: ./gradlew :solr:webapp:dependencies --write-locks -com.carrotsearch:hppc:0.10.0=solrCore -com.fasterxml.jackson.core:jackson-annotations:2.22=solrCore -com.fasterxml.jackson.core:jackson-core:2.22.0=solrCore -com.fasterxml.jackson.core:jackson-databind:2.22.0=solrCore -com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.0=solrCore -com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.22.0=solrCore -com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.22.0=solrCore -com.fasterxml.jackson:jackson-bom:2.22.0=solrCore -com.fasterxml.woodstox:woodstox-core:7.2.1=solrCore -com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,errorprone,solrCore,testAnnotationProcessor +com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.9.1=jarValidation,testCompileClasspath,testRuntimeClasspath +com.carrotsearch:hppc:0.10.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.22=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.22.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.22.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.22.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.22.0=jarValidation,solrCore,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.22.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +com.fasterxml.woodstox:woodstox-core:7.2.1=jarValidation,solrCore,testRuntimeClasspath +com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testRuntimeClasspath com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor +com.google.auto.service:auto-service-annotations:1.1.1=jarValidation,testCompileClasspath,testRuntimeClasspath com.google.auto.value:auto-value-annotations:1.11.1=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.47.0=solrCore +com.google.errorprone:error_prone_annotations:2.47.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.guava:failureaccess:1.0.3=annotationProcessor,errorprone,solrCore,testAnnotationProcessor -com.google.guava:guava:33.6.0-jre=annotationProcessor,errorprone,solrCore,testAnnotationProcessor -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,errorprone,solrCore,testAnnotationProcessor -com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,errorprone,solrCore,testAnnotationProcessor +com.google.guava:failureaccess:1.0.3=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:33.6.0-jre=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.protobuf:protobuf-java:4.35.1=annotationProcessor,errorprone,testAnnotationProcessor -com.j256.simplemagic:simplemagic:1.17=solrCore -com.jayway.jsonpath:json-path:3.0.0=solrCore +com.j256.simplemagic:simplemagic:1.17=jarValidation,solrCore,testRuntimeClasspath +com.jayway.jsonpath:json-path:3.0.0=jarValidation,solrCore,testRuntimeClasspath com.lmax:disruptor:4.0.0=serverLib -com.tdunning:t-digest:3.3=solrCore -commons-cli:commons-cli:1.11.0=solrCore -commons-codec:commons-codec:1.22.0=solrCore -commons-io:commons-io:2.22.0=solrCore -io.dropwizard.metrics:metrics-core:4.2.39=solrCore +com.tdunning:t-digest:3.3=jarValidation,solrCore,testRuntimeClasspath +commons-cli:commons-cli:1.11.0=jarValidation,solrCore,testRuntimeClasspath +commons-codec:commons-codec:1.22.0=jarValidation,solrCore,testRuntimeClasspath +commons-io:commons-io:2.22.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-core:4.2.39=jarValidation,solrCore,testRuntimeClasspath io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor -io.netty:netty-buffer:4.2.15.Final=solrCore -io.netty:netty-codec-base:4.2.15.Final=solrCore -io.netty:netty-common:4.2.15.Final=solrCore -io.netty:netty-handler:4.2.15.Final=solrCore -io.netty:netty-resolver:4.2.15.Final=solrCore -io.netty:netty-tcnative-boringssl-static:2.0.79.Final=solrCore -io.netty:netty-tcnative-classes:2.0.79.Final=solrCore -io.netty:netty-transport-classes-epoll:4.2.15.Final=solrCore -io.netty:netty-transport-native-epoll:4.2.15.Final=solrCore -io.netty:netty-transport-native-unix-common:4.2.15.Final=solrCore -io.netty:netty-transport:4.2.15.Final=solrCore -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.27.0-alpha=solrCore -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.27.0=solrCore -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.27.0-alpha=solrCore -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry:2.27.0-alpha=solrCore -io.opentelemetry.semconv:opentelemetry-semconv:1.40.0=solrCore -io.opentelemetry:opentelemetry-api-incubator:1.61.0-alpha=solrCore +io.netty:netty-buffer:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-base:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-tcnative-boringssl-static:2.0.79.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-tcnative-classes:2.0.79.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-classes-epoll:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-epoll:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.2.15.Final=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.27.0-alpha=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.27.0=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.27.0-alpha=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry:2.27.0-alpha=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry.semconv:opentelemetry-semconv:1.40.0=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry:opentelemetry-api-incubator:1.61.0-alpha=jarValidation,solrCore,testRuntimeClasspath io.opentelemetry:opentelemetry-api:1.63.0=solrCore +io.opentelemetry:opentelemetry-api:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-common:1.63.0=solrCore +io.opentelemetry:opentelemetry-common:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-context:1.63.0=solrCore -io.opentelemetry:opentelemetry-exporter-prometheus:1.63.0-alpha=solrCore +io.opentelemetry:opentelemetry-context:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-exporter-logging:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-exporter-prometheus:1.63.0-alpha=jarValidation,solrCore,testRuntimeClasspath io.opentelemetry:opentelemetry-sdk-common:1.63.0=solrCore +io.opentelemetry:opentelemetry-sdk-common:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-logs:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-sdk-metrics:1.63.0=solrCore +io.opentelemetry:opentelemetry-sdk-metrics:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-sdk-trace:1.63.0=solrCore +io.opentelemetry:opentelemetry-sdk-trace:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-sdk:1.63.0=solrCore -io.prometheus:prometheus-metrics-config:1.8.0=solrCore -io.prometheus:prometheus-metrics-exposition-formats:1.8.0=solrCore -io.prometheus:prometheus-metrics-exposition-textformats:1.8.0=solrCore -io.prometheus:prometheus-metrics-model:1.8.0=solrCore -io.sgr:s2-geometry-library-java:1.0.0=solrCore -io.swagger.core.v3:swagger-annotations-jakarta:2.2.52=solrCore -jakarta.activation:jakarta.activation-api:2.1.3=solrCore -jakarta.annotation:jakarta.annotation-api:3.0.0=solrCore -jakarta.inject:jakarta.inject-api:2.0.1=solrCore -jakarta.servlet:jakarta.servlet-api:6.1.0=serverLib,solrCore -jakarta.validation:jakarta.validation-api:3.1.0=solrCore -jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=solrCore -jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=solrCore +io.opentelemetry:opentelemetry-sdk:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.8.0=jarValidation,solrCore,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.8.0=jarValidation,solrCore,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.8.0=jarValidation,solrCore,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.8.0=jarValidation,solrCore,testRuntimeClasspath +io.sgr:s2-geometry-library-java:1.0.0=jarValidation,solrCore,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations-jakarta:2.2.52=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,solrCore,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:3.0.0=jarValidation,solrCore,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,solrCore,testRuntimeClasspath +jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,serverLib,solrCore,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.1.0=jarValidation,solrCore,testRuntimeClasspath +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,solrCore,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,solrCore,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor -org.antlr:antlr4-runtime:4.13.2=solrCore -org.apache.commons:commons-exec:1.6.0=solrCore -org.apache.commons:commons-lang3:3.20.0=solrCore -org.apache.commons:commons-math3:3.6.1=solrCore -org.apache.curator:curator-client:5.9.0=solrCore -org.apache.curator:curator-framework:5.9.0=solrCore +junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath +org.antlr:antlr4-runtime:4.13.2=jarValidation,solrCore,testRuntimeClasspath +org.apache.commons:commons-exec:1.6.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.commons:commons-lang3:3.20.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.commons:commons-math3:3.6.1=jarValidation,solrCore,testRuntimeClasspath +org.apache.curator:curator-client:5.9.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.curator:curator-framework:5.9.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.curator:curator-test:5.9.0=jarValidation,testRuntimeClasspath org.apache.logging.log4j:log4j-1.2-api:2.26.0=serverLib -org.apache.logging.log4j:log4j-api:2.26.0=serverLib,solrCore -org.apache.logging.log4j:log4j-core:2.26.0=serverLib,solrCore +org.apache.logging.log4j:log4j-api:2.26.0=jarValidation,serverLib,solrCore,testRuntimeClasspath +org.apache.logging.log4j:log4j-core:2.26.0=jarValidation,serverLib,solrCore,testRuntimeClasspath org.apache.logging.log4j:log4j-layout-template-json:2.26.0=serverLib -org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0=serverLib,solrCore +org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0=jarValidation,serverLib,solrCore,testRuntimeClasspath org.apache.logging.log4j:log4j-web:2.26.0=serverLib -org.apache.lucene:lucene-analysis-common:10.4.0=solrCore -org.apache.lucene:lucene-analysis-kuromoji:10.4.0=solrCore -org.apache.lucene:lucene-analysis-nori:10.4.0=solrCore -org.apache.lucene:lucene-analysis-phonetic:10.4.0=solrCore -org.apache.lucene:lucene-backward-codecs:10.4.0=solrCore -org.apache.lucene:lucene-classification:10.4.0=solrCore -org.apache.lucene:lucene-codecs:10.4.0=solrCore -org.apache.lucene:lucene-core:10.4.0=solrCore -org.apache.lucene:lucene-expressions:10.4.0=solrCore -org.apache.lucene:lucene-facet:10.4.0=solrCore -org.apache.lucene:lucene-grouping:10.4.0=solrCore -org.apache.lucene:lucene-highlighter:10.4.0=solrCore -org.apache.lucene:lucene-join:10.4.0=solrCore -org.apache.lucene:lucene-memory:10.4.0=solrCore -org.apache.lucene:lucene-misc:10.4.0=solrCore -org.apache.lucene:lucene-queries:10.4.0=solrCore -org.apache.lucene:lucene-queryparser:10.4.0=solrCore -org.apache.lucene:lucene-sandbox:10.4.0=solrCore -org.apache.lucene:lucene-spatial-extras:10.4.0=solrCore -org.apache.lucene:lucene-spatial3d:10.4.0=solrCore -org.apache.lucene:lucene-suggest:10.4.0=solrCore -org.apache.zookeeper:zookeeper-jute:3.9.5=solrCore -org.apache.zookeeper:zookeeper:3.9.5=solrCore -org.codehaus.woodstox:stax2-api:4.3.0=solrCore -org.eclipse.jetty.compression:jetty-compression-common:12.1.10=solrCore -org.eclipse.jetty.compression:jetty-compression-gzip:12.1.10=solrCore -org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.10=serverLib +org.apache.lucene:lucene-analysis-common:10.4.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-classification:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-codecs:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-core:10.4.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.lucene:lucene-expressions:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-facet:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-grouping:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-highlighter:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-join:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-memory:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-misc:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-queries:10.4.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.lucene:lucene-queryparser:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-sandbox:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-spatial-extras:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-spatial3d:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-suggest:10.4.0=jarValidation,solrCore,testRuntimeClasspath +org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.apache.zookeeper:zookeeper-jute:3.9.5=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apache.zookeeper:zookeeper:3.9.5=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath +org.codehaus.woodstox:stax2-api:4.3.0=jarValidation,solrCore,testRuntimeClasspath +org.eclipse.jetty.compression:jetty-compression-common:12.1.10=jarValidation,solrCore,testRuntimeClasspath +org.eclipse.jetty.compression:jetty-compression-gzip:12.1.10=jarValidation,solrCore,testRuntimeClasspath +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.10=jarValidation,serverLib,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlets:12.1.10=serverLib org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.10=serverLib org.eclipse.jetty.ee:jetty-ee-webapp:12.1.10=serverLib -org.eclipse.jetty.http2:jetty-http2-client-transport:12.1.10=solrCore -org.eclipse.jetty.http2:jetty-http2-client:12.1.10=solrCore -org.eclipse.jetty.http2:jetty-http2-common:12.1.10=serverLib,solrCore -org.eclipse.jetty.http2:jetty-http2-hpack:12.1.10=serverLib,solrCore -org.eclipse.jetty.http2:jetty-http2-server:12.1.10=serverLib -org.eclipse.jetty:jetty-alpn-client:12.1.10=solrCore -org.eclipse.jetty:jetty-alpn-java-client:12.1.10=solrCore -org.eclipse.jetty:jetty-alpn-java-server:12.1.10=serverLib -org.eclipse.jetty:jetty-alpn-server:12.1.10=serverLib -org.eclipse.jetty:jetty-client:12.1.10=solrCore +org.eclipse.jetty.http2:jetty-http2-client-transport:12.1.10=jarValidation,solrCore,testRuntimeClasspath +org.eclipse.jetty.http2:jetty-http2-client:12.1.10=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty.http2:jetty-http2-common:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty.http2:jetty-http2-hpack:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty.http2:jetty-http2-server:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-alpn-client:12.1.10=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-alpn-java-client:12.1.10=jarValidation,solrCore,testRuntimeClasspath +org.eclipse.jetty:jetty-alpn-java-server:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-alpn-server:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-client:12.1.10=jarValidation,solrCore,testRuntimeClasspath org.eclipse.jetty:jetty-deploy:12.1.10=serverLib -org.eclipse.jetty:jetty-http:12.1.10=serverLib,solrCore -org.eclipse.jetty:jetty-io:12.1.10=serverLib,solrCore +org.eclipse.jetty:jetty-http:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-io:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-jmx:12.1.10=serverLib -org.eclipse.jetty:jetty-rewrite:12.1.10=serverLib -org.eclipse.jetty:jetty-security:12.1.10=serverLib,solrCore -org.eclipse.jetty:jetty-server:12.1.10=serverLib,solrCore -org.eclipse.jetty:jetty-session:12.1.10=serverLib -org.eclipse.jetty:jetty-util:12.1.10=serverLib,solrCore +org.eclipse.jetty:jetty-rewrite:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-security:12.1.10=jarValidation,serverLib,solrCore,testRuntimeClasspath +org.eclipse.jetty:jetty-server:12.1.10=jarValidation,serverLib,solrCore,testRuntimeClasspath +org.eclipse.jetty:jetty-session:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-util:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-xml:12.1.10=serverLib -org.glassfish.hk2.external:aopalliance-repackaged:4.0.1=solrCore -org.glassfish.hk2:hk2-api:4.0.1=solrCore -org.glassfish.hk2:hk2-locator:4.0.1=solrCore -org.glassfish.hk2:hk2-utils:4.0.1=solrCore -org.glassfish.hk2:osgi-resource-locator:3.0.0=solrCore -org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=solrCore -org.glassfish.jersey.core:jersey-client:4.0.2=solrCore -org.glassfish.jersey.core:jersey-common:4.0.2=solrCore -org.glassfish.jersey.core:jersey-server:4.0.2=solrCore -org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=solrCore -org.glassfish.jersey.inject:jersey-hk2:4.0.2=solrCore -org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=solrCore -org.glassfish.jersey:jersey-bom:4.0.2=solrCore -org.javassist:javassist:3.30.2-GA=solrCore +org.glassfish.hk2.external:aopalliance-repackaged:4.0.1=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.hk2:hk2-api:4.0.1=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.hk2:hk2-locator:4.0.1=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.hk2:hk2-utils:4.0.1=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.hk2:osgi-resource-locator:3.0.0=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.core:jersey-client:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.core:jersey-common:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.core:jersey-server:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.glassfish.jersey:jersey-bom:4.0.2=jarValidation,solrCore,testRuntimeClasspath +org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.javassist:javassist:3.30.2-GA=jarValidation,solrCore,testRuntimeClasspath org.jspecify:jspecify:1.0.0=annotationProcessor,errorprone,solrCore,testAnnotationProcessor -org.locationtech.spatial4j:spatial4j:0.8=solrCore -org.ow2.asm:asm-commons:9.10.1=solrCore -org.ow2.asm:asm-tree:9.10.1=solrCore -org.ow2.asm:asm:9.10.1=solrCore +org.jspecify:jspecify:1.0.1=jarValidation,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath +org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath +org.locationtech.spatial4j:spatial4j:0.8=jarValidation,solrCore,testRuntimeClasspath +org.opentest4j:opentest4j:1.2.0=jarValidation,testRuntimeClasspath +org.ow2.asm:asm-commons:9.10.1=jarValidation,solrCore,testRuntimeClasspath +org.ow2.asm:asm-tree:9.10.1=jarValidation,solrCore,testRuntimeClasspath +org.ow2.asm:asm:9.10.1=jarValidation,solrCore,testRuntimeClasspath org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor -org.semver4j:semver4j:6.0.0=solrCore -org.slf4j:jcl-over-slf4j:2.0.17=serverLib,solrCore +org.seleniumhq.selenium:selenium-api:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-chrome-driver:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-chromium-driver:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-http:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-json:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-manager:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-os:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-remote-driver:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.seleniumhq.selenium:selenium-support:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath +org.semver4j:semver4j:6.0.0=jarValidation,solrCore,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,serverLib,solrCore,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.17=serverLib -org.slf4j:slf4j-api:2.0.17=serverLib,solrCore -org.xerial.snappy:snappy-java:1.1.10.8=solrCore -empty=compileClasspath,generatedJSClientBundle,generatedUIBundle,jarValidation,missingdoclet,providedCompile,providedRuntime,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,war +org.slf4j:slf4j-api:2.0.17=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,solrCore,testRuntimeClasspath +empty=compileClasspath,generatedJSClientBundle,generatedUIBundle,missingdoclet,providedCompile,providedRuntime,runtimeClasspath,war diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDashboardTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDashboardTest.java new file mode 100644 index 000000000000..2ec93391acf2 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDashboardTest.java @@ -0,0 +1,61 @@ +/* + * 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.webapp; + +import java.util.Map; +import org.apache.solr.common.util.NamedList; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Tests that the Admin UI dashboard (route {@code #/}) renders the correct Solr version and system + * stats, compared against the {@code /admin/info/system} API the screen is built from. + */ +public class AdminUiDashboardTest extends AdminUiTestBase { + + @Test + public void testDashboardShowsVersionsAndSystemStats() throws Exception { + NamedList system = adminApi("/admin/info/system", params()); + Map lucene = (Map) system.get("lucene"); + Map jvm = (Map) system.get("jvm"); + + openPage("", By.id("index")); + + // Versions block matches the API values + String solrSpecVersion = (String) lucene.get("solr-spec-version"); + assertEquals(solrSpecVersion, waitForText(By.cssSelector("#versions .solr_spec_version dd"))); + String luceneSpecVersion = (String) lucene.get("lucene-spec-version"); + assertEquals( + luceneSpecVersion, waitForText(By.cssSelector("#versions .lucene_spec_version dd"))); + + // JVM block shows the runtime name and version + String jvmText = waitForText(By.cssSelector("#jvm .jvm_version dd")); + assertEquals(jvm.get("name") + " " + jvm.get("version"), jvmText); + + // JVM memory bar is rendered with a non-empty max value + String jvmMemoryMax = waitForText(By.cssSelector("#jvm-memory-bar .bar-max.val")); + assertFalse("JVM memory bar should show a max value", jvmMemoryMax.isBlank()); + + // Security block warns that security is not enabled on this vanilla cluster + String securityText = waitFor(By.cssSelector("#security .warning-msg")).getText(); + assertTrue( + "Expected security warning, got: " + securityText, + securityText.contains("Security is not enabled")); + + assertNoSevereConsoleErrors(); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java new file mode 100644 index 000000000000..47182ce15dac --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -0,0 +1,272 @@ +/* + * 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.webapp; + +import com.carrotsearch.randomizedtesting.ThreadFilter; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.logging.Level; +import org.apache.lucene.tests.util.QuickPatchThreadsFilter; +import org.apache.lucene.util.SuppressForbidden; +import org.apache.solr.SolrIgnoredThreadsFilter; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.openqa.selenium.By; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.openqa.selenium.logging.LoggingPreferences; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for browser-based tests of the AngularJS Admin UI. + * + *

Starts a {@link SolrCloudTestCase} mini-cluster whose Jetty nodes also serve the Admin UI + * static files (see {@code JettyConfig.Builder#enableAdminUi(boolean)}), then drives the UI with a + * headless Chrome via Selenium WebDriver. + * + *

The tests require a locally installed Chrome/Chromium browser. Discovery order: the {@code + * tests.ui.chrome.binary} system property, the {@code CHROME_BIN} environment variable, then a list + * of well-known install locations. When no browser is found, all tests in the class are skipped via + * {@link Assume}. The matching chromedriver is provisioned by Selenium Manager, which may download + * it on first use (cached under {@code ~/.cache/selenium}); if that fails (e.g. offline), tests are + * likewise skipped. + */ +@SolrTestCaseJ4.SuppressSSL(bugUrl = "Admin UI browser tests drive plain http") +@ThreadLeakFilters( + defaultFilters = true, + filters = { + SolrIgnoredThreadsFilter.class, + QuickPatchThreadsFilter.class, + AdminUiTestBase.WebDriverThreadsFilter.class + }) +@ThreadLeakLingering(linger = 5000) +public abstract class AdminUiTestBase extends SolrCloudTestCase { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15); + + protected static WebDriver driver; + + /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ + protected static String baseUrl; + + /** Ignores threads spawned by Selenium and the JDK http client it uses. */ + public static class WebDriverThreadsFilter implements ThreadFilter { + @Override + public boolean reject(Thread t) { + String name = t.getName(); + // JDK java.net.http client worker/selector threads (used by Selenium) are daemon + // threads in a shared pool that outlive WebDriver.quit() + return name.startsWith("HttpClient-") + // reaps the external chromedriver/chrome processes + || name.startsWith("process reaper") + // selenium driver-service startup checker pool, terminates on its own + || name.startsWith("UrlChecker-") + // JDK-internal scheduler backing CompletableFuture timeouts, lives forever + || name.equals("CompletableFutureDelayScheduler"); + } + } + + @BeforeClass + public static void startClusterAndBrowser() throws Exception { + Path chrome = findChromeBinary(); + Assume.assumeTrue( + "No Chrome/Chromium binary found (set -Dtests.ui.chrome.binary=...), skipping UI tests", + chrome != null); + + configureCluster(2).withJettyConfig(jetty -> jetty.enableAdminUi(true)).configure(); + baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); + + ChromeOptions options = new ChromeOptions(); + options.setBinary(chrome.toString()); + options.addArguments( + "--headless=new", + "--window-size=1440,1024", + "--disable-gpu", + "--no-sandbox", + "--disable-dev-shm-usage"); + LoggingPreferences logPrefs = new LoggingPreferences(); + logPrefs.enable(LogType.BROWSER, Level.ALL); + options.setCapability("goog:loggingPrefs", logPrefs); + try { + driver = new ChromeDriver(options); + } catch (WebDriverException e) { + Assume.assumeNoException( + "Could not start ChromeDriver (chromedriver missing and not downloadable?)", e); + } + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30)); + } + + @AfterClass + public static void stopBrowser() { + if (driver != null) { + try { + driver.quit(); + } finally { + driver = null; + baseUrl = null; + } + } + } + + /** Captures a screenshot and the page source when a test fails, for post-mortem debugging. */ + @Rule + public final TestRule screenshotOnFailure = + new TestWatcher() { + @Override + protected void failed(Throwable e, Description description) { + if (driver == null) return; + try { + Path dir = createTempDir("ui-failure-" + description.getMethodName()); + byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + Files.write(dir.resolve("screenshot.png"), png); + Files.writeString(dir.resolve("page.html"), driver.getPageSource()); + log.error("UI test failure artifacts saved to {}", dir); + } catch (Exception suppressed) { + log.warn("Could not save UI failure artifacts", suppressed); + } + } + }; + + /** + * Navigates to an Admin UI page and waits for a screen-specific anchor element to be visible. + * + * @param route the Angular hash route without leading {@code #/}, e.g. {@code ""} (dashboard), + * {@code "~cloud"} or {@code "collection1/query"} + * @param anchor a locator for an element that indicates the screen has rendered + * @return the anchor element + */ + protected static WebElement openPage(String route, By anchor) { + driver.get(baseUrl + "/index.html#/" + route); + return waitFor(anchor); + } + + /** Waits for the given element to be visible, up to {@link #WAIT_TIMEOUT}. */ + protected static WebElement waitFor(By locator) { + return new WebDriverWait(driver, WAIT_TIMEOUT) + .until(ExpectedConditions.visibilityOfElementLocated(locator)); + } + + /** Waits until the given condition on an element's text holds, and returns the text. */ + protected static String waitForText(By locator) { + new WebDriverWait(driver, WAIT_TIMEOUT) + .until( + d -> { + WebElement el = d.findElement(locator); + return el != null && !el.getText().isBlank(); + }); + return driver.findElement(locator).getText(); + } + + /** + * Issues a GET request to the given admin path (e.g. {@code /admin/info/system}) on the same node + * the browser talks to, and returns the parsed response. Used to fetch the expected values that + * the UI should display. + */ + protected static NamedList adminApi(String path, SolrParams params) + throws IOException, org.apache.solr.client.solrj.SolrServerException { + try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { + return client.request(new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)); + } + } + + /** Fails the test if the browser console contains SEVERE errors (ignoring known-benign ones). */ + protected static void assertNoSevereConsoleErrors() { + List entries = driver.manage().logs().get(LogType.BROWSER).getAll(); + List severe = + entries.stream() + .filter(entry -> entry.getLevel().intValue() >= Level.SEVERE.intValue()) + .filter(entry -> !entry.getMessage().contains("favicon.ico")) + // the js-client bundle is generated into the war at build time and does not + // exist in the source tree that tests serve from; the UI degrades gracefully + .filter(entry -> !entry.getMessage().contains("libs/solr/index.js")) + .toList(); + assertTrue("Severe browser console errors: " + severe, severe.isEmpty()); + } + + /** Locates a Chrome/Chromium binary, or returns null if none can be found. */ + @SuppressForbidden(reason = "Reading CHROME_BIN/PATH from the environment to locate a browser") + protected static Path findChromeBinary() { + String sysProp = System.getProperty("tests.ui.chrome.binary"); + if (sysProp != null) { + Path path = Path.of(sysProp); + return Files.isExecutable(path) ? path : null; + } + String envBin = System.getenv("CHROME_BIN"); + if (envBin != null && Files.isExecutable(Path.of(envBin))) { + return Path.of(envBin); + } + List wellKnown = + List.of( + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/snap/bin/chromium", + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"); + for (String candidate : wellKnown) { + Path path = Path.of(candidate); + if (Files.isExecutable(path)) { + return path; + } + } + String pathEnv = System.getenv("PATH"); + if (pathEnv != null) { + for (String dir : pathEnv.split(java.io.File.pathSeparator)) { + for (String name : List.of("google-chrome", "chromium", "chromium-browser")) { + Path path = Path.of(dir, name); + if (Files.isExecutable(path)) { + return path; + } + } + } + } + return null; + } +} From 7131cbfc7ba00de1ab57948efd9db2c1202e1733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:26:13 +0200 Subject: [PATCH 02/30] Add test plan document for Admin UI browser tests --- dev-docs/admin-ui-tests.md | 111 +++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 dev-docs/admin-ui-tests.md diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md new file mode 100644 index 000000000000..be27a586e14c --- /dev/null +++ b/dev-docs/admin-ui-tests.md @@ -0,0 +1,111 @@ + + +# Admin UI (AngularJS) Browser Test Plan + +This document tracks browser-based test coverage of the old AngularJS Admin UI +(`solr/webapp/web/`), driven by Selenium WebDriver with headless Chrome. + +## How the tests work + +- Tests live in `solr/webapp/src/test/org/apache/solr/webapp/` and extend + `AdminUiTestBase`, which starts a 2-node `MiniSolrCloudCluster` whose Jetty + nodes also serve the Admin UI (opt-in `JettyConfig.Builder#enableAdminUi`), + then starts a headless Chrome via Selenium WebDriver. +- A locally installed Chrome/Chromium is required; without one, tests skip via + `Assume`. Override discovery with `-Dtests.ui.chrome.binary=/path/to/chrome`. + The matching chromedriver is provisioned (and cached) by Selenium Manager. +- Display assertions compare UI text against live JSON from the same node's + admin APIs — never hardcoded values. +- Run with: `./gradlew :solr:webapp:test` + +## Phase 1 — Smoke/navigation (`AdminUiSmokeTest`) + +Navigate every route, wait for a screen-specific anchor element, assert no +severe browser console errors. + +- [ ] Node-level routes: `/`, `~logging`, `~logging/level`, `~cloud?view=nodes`, + `~cloud?view=tree`, `~cloud?view=zkstatus`, `~cloud?view=graph`, `~cores`, + `~collections`, `~schema-designer`, `~security`, `~java-properties`, + `~threads`, `login` +- [ ] Per-collection routes (fixture collection): `collection-overview`, + `analysis`, `documents`, `files`, `query`, `stream`, `paramsets`, + `plugins`, `schema`, `segments` + +Flaky-risk flags: `~cloud?view=graph` (d3 svg async), `~cloud?view=zkstatus` +(ZK admin-command availability in the embedded ensemble), `~schema-designer` +(many chained requests), `sqlquery` (needs sql module — excluded). + +## Phase 2 — Node-level screens, display depth + +- [x] Dashboard (`AdminUiDashboardTest`): versions, JVM info, memory bars, + security warning vs `/admin/info/system` +- [ ] Java Properties: a few props from `/admin/info/properties` rendered +- [ ] Thread Dump: thread list non-empty, known thread name, expand stacktrace +- [ ] Logging: logger tree renders, `org.apache.solr` row with level +- [ ] Cloud > Nodes: both nodes listed, host:port match cluster +- [ ] Cloud > Tree: `/live_nodes` count matches, expand collection `state.json` +- [ ] Cloud > ZK Status: ensemble status shown (lenient assertions) +- [ ] Cloud > Graph: collection node and replica leaves in SVG (lenient) +- [ ] Collections: created collection listed; detail shows shards/replicas Active +- [ ] Core Admin: core selector lists the core, overview matches `/admin/cores` +- [ ] Security: "security is not enabled" warning panel (no auth configured) +- [ ] Login: not-authenticated info page when no authenticationPlugin + +## Phase 3 — Per-collection screens (fixture: collection with pre-indexed docs) + +- [ ] Collection Overview: numDocs/maxDoc match API, healthy replica badge +- [ ] Query: run `*:*` via form, response block shows expected `numFound`; + change `rows` and re-run +- [ ] Analysis: analyze a value for `text_general`, token table lowercases +- [ ] Documents (display): form renders, doc-type dropdown options present +- [ ] Schema Browser: field list contains `id`, flags match `/schema` API, + term info loads for a populated field +- [ ] Files: tree lists `solrconfig.xml`, content loads +- [ ] Plugins/Stats: categories listed, searcher stats show numDocs +- [ ] Segments: segment bars present after commit +- [ ] Paramsets (display): empty-state or created paramset shown +- [ ] Stream: simple streaming expression executes and renders result (medium risk) +- [ ] Replication in cloud mode: verify what the screen shows; standalone-mode + coverage deferred + +## Phase 4 — Write actions through the UI + +- [ ] Collections: create collection via dialog → verify via CLUSTERSTATUS → + delete via UI → gone. Create/delete alias. Add replica. +- [ ] Documents: submit JSON doc → success response → found via UI Query and SolrJ +- [ ] Schema Browser: add field → verify in UI and `/schema/fields` → delete field +- [ ] Logging: set a logger to WARN via level editor → verify via API → revert +- [ ] Core Admin: RELOAD core via UI (rename/swap/unload deferred to a + standalone-mode class) +- [ ] Paramsets: create paramset via UI → verify via `/config/params` +- [ ] Security with BasicAuth (`@Nightly`): bootstrap `security.json`, login via + form, add user/role/permission, verify via security APIs (high flake risk) +- [ ] Schema Designer happy path (`@Nightly`, high flake risk) + +Policy: phases 1–3 run in the default test run; heavyweight phase-4 classes +(security, schema designer) are `@Nightly`. + +## Known limitations + +- The generated js-client bundle (`libs/solr/index.js`) only exists inside the + built WAR, not in the source tree tests serve from; its 404 is whitelisted in + the console-error assertion and v2-API-backed UI features relying on it are + not exercised. +- ASF Jenkins has no Chrome, so these tests skip there; they run on developer + machines and could run in a GitHub Actions workflow (Chrome preinstalled on + `ubuntu-latest`) as a follow-up. From a350a2a91e84dd306ef0b8f91d369ef97e4c6656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:34:33 +0200 Subject: [PATCH 03/30] Slim the Selenium dependency footprint Exclude opentelemetry (provided by solr-core), auto-service and jspecify annotation jars, and drop selenium-support in favor of a small poll-based wait helper. Only the 8 core selenium jars and a byte-buddy version bump remain as new dependencies. --- gradle/libs.versions.toml | 1 - solr/licenses/auto-service-LICENSE-ASL.txt | 176 ------------------ solr/licenses/auto-service-NOTICE.txt | 2 - .../auto-service-annotations-1.1.1.jar.sha1 | 1 - solr/licenses/jspecify-1.0.1.jar.sha1 | 1 - .../opentelemetry-api-1.65.0.jar.sha1 | 1 - .../opentelemetry-common-1.65.0.jar.sha1 | 1 - .../opentelemetry-context-1.65.0.jar.sha1 | 1 - ...telemetry-exporter-logging-1.65.0.jar.sha1 | 1 - .../opentelemetry-sdk-1.65.0.jar.sha1 | 1 - .../opentelemetry-sdk-common-1.65.0.jar.sha1 | 1 - ...dk-extension-autoconfigure-1.65.0.jar.sha1 | 1 - ...xtension-autoconfigure-spi-1.65.0.jar.sha1 | 1 - .../opentelemetry-sdk-logs-1.65.0.jar.sha1 | 1 - .../opentelemetry-sdk-metrics-1.65.0.jar.sha1 | 1 - .../opentelemetry-sdk-trace-1.65.0.jar.sha1 | 1 - .../licenses/selenium-support-4.47.0.jar.sha1 | 1 - solr/webapp/build.gradle | 11 +- solr/webapp/gradle.lockfile | 30 +-- .../apache/solr/webapp/AdminUiTestBase.java | 68 +++++-- 20 files changed, 71 insertions(+), 231 deletions(-) delete mode 100644 solr/licenses/auto-service-LICENSE-ASL.txt delete mode 100644 solr/licenses/auto-service-NOTICE.txt delete mode 100644 solr/licenses/auto-service-annotations-1.1.1.jar.sha1 delete mode 100644 solr/licenses/jspecify-1.0.1.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-api-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-common-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-context-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 delete mode 100644 solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 delete mode 100644 solr/licenses/selenium-support-4.47.0.jar.sha1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59c631130ad4..d8b039b474ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -514,7 +514,6 @@ prometheus-metrics-expositionformats = { module = "io.prometheus:prometheus-metr prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus-metrics" } quicktheories-quicktheories = { module = "org.quicktheories:quicktheories", version.ref = "quicktheories" } selenium-chromedriver = { module = "org.seleniumhq.selenium:selenium-chrome-driver", version.ref = "selenium" } -selenium-support = { module = "org.seleniumhq.selenium:selenium-support", version.ref = "selenium" } semver4j-semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jcloverslf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref = "slf4j" } diff --git a/solr/licenses/auto-service-LICENSE-ASL.txt b/solr/licenses/auto-service-LICENSE-ASL.txt deleted file mode 100644 index d0381d6d04c7..000000000000 --- a/solr/licenses/auto-service-LICENSE-ASL.txt +++ /dev/null @@ -1,176 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/solr/licenses/auto-service-NOTICE.txt b/solr/licenses/auto-service-NOTICE.txt deleted file mode 100644 index e053b41122dc..000000000000 --- a/solr/licenses/auto-service-NOTICE.txt +++ /dev/null @@ -1,2 +0,0 @@ -AutoService -Copyright 2013 Google LLC diff --git a/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 b/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 deleted file mode 100644 index 5d49902ad62e..000000000000 --- a/solr/licenses/auto-service-annotations-1.1.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -da12a15cd058ba90a0ff55357fb521161af4736d diff --git a/solr/licenses/jspecify-1.0.1.jar.sha1 b/solr/licenses/jspecify-1.0.1.jar.sha1 deleted file mode 100644 index b901c10940be..000000000000 --- a/solr/licenses/jspecify-1.0.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -3d60fd98eb8ade73004f4195c37b6317e02cf3d7 diff --git a/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 deleted file mode 100644 index a78665451042..000000000000 --- a/solr/licenses/opentelemetry-api-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8b5df7f216b8b75da02f4f70daef71e70c91ff5e diff --git a/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 deleted file mode 100644 index abef04ed4c6d..000000000000 --- a/solr/licenses/opentelemetry-common-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -843f221202a008c893e18dafed0a024ef9d25ac1 diff --git a/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 deleted file mode 100644 index 963bf088700f..000000000000 --- a/solr/licenses/opentelemetry-context-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -d62950109b08e183ee6e397394a2443bc6bdbe84 diff --git a/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 deleted file mode 100644 index 7c21f4ad37c0..000000000000 --- a/solr/licenses/opentelemetry-exporter-logging-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -f15158bde45aab36263dd000d7401e9ca1a97455 diff --git a/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 deleted file mode 100644 index 36f68b3eb539..000000000000 --- a/solr/licenses/opentelemetry-sdk-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0ec81a4855a64cd088f3aa0de5bf581717565838 diff --git a/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 deleted file mode 100644 index 086b29c0fa5d..000000000000 --- a/solr/licenses/opentelemetry-sdk-common-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7dbe712d9b9f51a48022f1cd358c0ed7e80f6a06 diff --git a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 deleted file mode 100644 index 6eacefb05e36..000000000000 --- a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -28ae444a769d8cade194ca5bbdd6924a56b1d513 diff --git a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 deleted file mode 100644 index 86cb298b9cd8..000000000000 --- a/solr/licenses/opentelemetry-sdk-extension-autoconfigure-spi-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -635051e4ba91ea38350e61ed58a39d16d573e977 diff --git a/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 deleted file mode 100644 index fa27a1b36c23..000000000000 --- a/solr/licenses/opentelemetry-sdk-logs-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -9982196f3ef9531cb985ae951eb5eba952959793 diff --git a/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 deleted file mode 100644 index 8fa8f2bd8552..000000000000 --- a/solr/licenses/opentelemetry-sdk-metrics-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -75298219ef305b42193cb40ff1e72e0cf8df8f4f diff --git a/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 b/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 deleted file mode 100644 index 35c23f879151..000000000000 --- a/solr/licenses/opentelemetry-sdk-trace-1.65.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -581326fd733b17cdd37e005575073e44a0a9b8ba diff --git a/solr/licenses/selenium-support-4.47.0.jar.sha1 b/solr/licenses/selenium-support-4.47.0.jar.sha1 deleted file mode 100644 index dd9100813969..000000000000 --- a/solr/licenses/selenium-support-4.47.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -5b182dce28b0f22ee7a3afa1a7966d6d83ef18f5 diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle index 71691b2260f6..5430dbaca3cf 100644 --- a/solr/webapp/build.gradle +++ b/solr/webapp/build.gradle @@ -56,8 +56,15 @@ dependencies { testImplementation project(':solr:test-framework') testImplementation libs.carrotsearch.randomizedtesting.runner testImplementation libs.junit.junit - testImplementation libs.selenium.chromedriver - testImplementation libs.selenium.support + testImplementation(libs.selenium.chromedriver, { + // solr-core already provides the opentelemetry api/sdk that selenium's optional + // tracing uses; the remaining excludes are compile-time-only annotation jars + exclude group: 'io.opentelemetry' + exclude group: 'io.opentelemetry.instrumentation' + exclude group: 'io.opentelemetry.semconv' + exclude group: 'com.google.auto.service' + exclude group: 'org.jspecify' + }) } // Forward the browser-binary override for the Admin UI tests to the forked test JVM diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile index b655bafc1349..63a691f47c3c 100644 --- a/solr/webapp/gradle.lockfile +++ b/solr/webapp/gradle.lockfile @@ -15,7 +15,6 @@ com.fasterxml.woodstox:woodstox-core:7.2.1=jarValidation,solrCore,testRuntimeCla com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testRuntimeClasspath com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor -com.google.auto.service:auto-service-annotations:1.1.1=jarValidation,testCompileClasspath,testRuntimeClasspath com.google.auto.value:auto-value-annotations:1.11.1=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor @@ -56,25 +55,14 @@ io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.27.0-a io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry:2.27.0-alpha=jarValidation,solrCore,testRuntimeClasspath io.opentelemetry.semconv:opentelemetry-semconv:1.40.0=jarValidation,solrCore,testRuntimeClasspath io.opentelemetry:opentelemetry-api-incubator:1.61.0-alpha=jarValidation,solrCore,testRuntimeClasspath -io.opentelemetry:opentelemetry-api:1.63.0=solrCore -io.opentelemetry:opentelemetry-api:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-common:1.63.0=solrCore -io.opentelemetry:opentelemetry-common:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-context:1.63.0=solrCore -io.opentelemetry:opentelemetry-context:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-logging:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-api:1.63.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-common:1.63.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-context:1.63.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-exporter-prometheus:1.63.0-alpha=jarValidation,solrCore,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-common:1.63.0=solrCore -io.opentelemetry:opentelemetry-sdk-common:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-logs:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-metrics:1.63.0=solrCore -io.opentelemetry:opentelemetry-sdk-metrics:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-trace:1.63.0=solrCore -io.opentelemetry:opentelemetry-sdk-trace:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk:1.63.0=solrCore -io.opentelemetry:opentelemetry-sdk:1.65.0=jarValidation,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-common:1.63.0=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-metrics:1.63.0=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk-trace:1.63.0=jarValidation,solrCore,testRuntimeClasspath +io.opentelemetry:opentelemetry-sdk:1.63.0=jarValidation,solrCore,testRuntimeClasspath io.prometheus:prometheus-metrics-config:1.8.0=jarValidation,solrCore,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-formats:1.8.0=jarValidation,solrCore,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-textformats:1.8.0=jarValidation,solrCore,testRuntimeClasspath @@ -171,8 +159,7 @@ org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=jarValidation,solrCor org.glassfish.jersey:jersey-bom:4.0.2=jarValidation,solrCore,testRuntimeClasspath org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testRuntimeClasspath org.javassist:javassist:3.30.2-GA=jarValidation,solrCore,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=annotationProcessor,errorprone,solrCore,testAnnotationProcessor -org.jspecify:jspecify:1.0.1=jarValidation,testCompileClasspath,testRuntimeClasspath +org.jspecify:jspecify:1.0.0=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath @@ -190,7 +177,6 @@ org.seleniumhq.selenium:selenium-json:4.47.0=jarValidation,testCompileClasspath, org.seleniumhq.selenium:selenium-manager:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath org.seleniumhq.selenium:selenium-os:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath org.seleniumhq.selenium:selenium-remote-driver:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.seleniumhq.selenium:selenium-support:4.47.0=jarValidation,testCompileClasspath,testRuntimeClasspath org.semver4j:semver4j:6.0.0=jarValidation,solrCore,testRuntimeClasspath org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,serverLib,solrCore,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.17=serverLib diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index 47182ce15dac..40bd792a7146 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -44,7 +44,9 @@ import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.OutputType; +import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; @@ -54,8 +56,6 @@ import org.openqa.selenium.logging.LogEntry; import org.openqa.selenium.logging.LogType; import org.openqa.selenium.logging.LoggingPreferences; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,6 +117,9 @@ public static void startClusterAndBrowser() throws Exception { "No Chrome/Chromium binary found (set -Dtests.ui.chrome.binary=...), skipping UI tests", chrome != null); + // metrics are off by default in test clusters, but UI screens (e.g. Plugins) need them; + // restored after the class by SolrTestCase's SystemPropertiesRestoreRule + System.setProperty("metricsEnabled", "true"); configureCluster(2).withJettyConfig(jetty -> jetty.enableAdminUi(true)).configure(); baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); @@ -186,19 +189,47 @@ protected static WebElement openPage(String route, By anchor) { /** Waits for the given element to be visible, up to {@link #WAIT_TIMEOUT}. */ protected static WebElement waitFor(By locator) { - return new WebDriverWait(driver, WAIT_TIMEOUT) - .until(ExpectedConditions.visibilityOfElementLocated(locator)); + return poll(locator, el -> el.isDisplayed() ? el : null, "visible element"); } - /** Waits until the given condition on an element's text holds, and returns the text. */ + /** Waits until the given element has non-blank text, and returns the text. */ protected static String waitForText(By locator) { - new WebDriverWait(driver, WAIT_TIMEOUT) - .until( - d -> { - WebElement el = d.findElement(locator); - return el != null && !el.getText().isBlank(); - }); - return driver.findElement(locator).getText(); + return poll( + locator, + el -> { + String text = el.getText(); + return el.isDisplayed() && !text.isBlank() ? text : null; + }, + "non-empty text"); + } + + /** + * Polls the given element until {@code condition} returns non-null (a fresh lookup each round, so + * elements replaced by Angular re-renders are tolerated), failing after {@link #WAIT_TIMEOUT}. + */ + private static T poll( + By locator, java.util.function.Function condition, String description) { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + WebDriverException lastException = null; + while (System.nanoTime() < deadlineNanos) { + try { + T result = condition.apply(driver.findElement(locator)); + if (result != null) { + return result; + } + lastException = null; + } catch (NoSuchElementException | StaleElementReferenceException e) { + lastException = e; + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new AssertionError( + "Timed out waiting for " + description + " at " + locator, lastException); } /** @@ -213,16 +244,25 @@ protected static NamedList adminApi(String path, SolrParams params) } } - /** Fails the test if the browser console contains SEVERE errors (ignoring known-benign ones). */ - protected static void assertNoSevereConsoleErrors() { + /** + * Fails the test if the browser console contains SEVERE errors, ignoring known-benign ones and + * any messages containing one of {@code allowedSubstrings}. + */ + protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { List entries = driver.manage().logs().get(LogType.BROWSER).getAll(); List severe = entries.stream() .filter(entry -> entry.getLevel().intValue() >= Level.SEVERE.intValue()) + .filter( + entry -> + java.util.Arrays.stream(allowedSubstrings) + .noneMatch(allowed -> entry.getMessage().contains(allowed))) .filter(entry -> !entry.getMessage().contains("favicon.ico")) // the js-client bundle is generated into the war at build time and does not // exist in the source tree that tests serve from; the UI degrades gracefully .filter(entry -> !entry.getMessage().contains("libs/solr/index.js")) + // "solrApi" is defined by that same missing js-client bundle + .filter(entry -> !entry.getMessage().contains("solrApi is not defined")) .toList(); assertTrue("Severe browser console errors: " + severe, severe.isEmpty()); } From 5b8cada5656a856488600df2c3ccd24cfb41483d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:34:33 +0200 Subject: [PATCH 04/30] Admin UI tests: smoke-test navigation of all screens Navigates every node-level, cloud, collection and core screen, waiting for each screen's main content element and asserting no severe browser console errors. Runs the test cluster with metricsEnabled=true so metrics-backed screens (Plugins) work. --- .../apache/solr/webapp/AdminUiSmokeTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java new file mode 100644 index 000000000000..badd47151a63 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java @@ -0,0 +1,116 @@ +/* + * 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.webapp; + +import java.util.Map; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.util.ExternalPaths; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Smoke test navigating every screen of the Admin UI, asserting that each renders its main content + * element without severe browser console errors. + */ +public class AdminUiSmokeTest extends AdminUiTestBase { + + private static final String COLLECTION = "smoke"; + + @BeforeClass + public static void setupCollection() throws Exception { + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); + CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 2) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(COLLECTION, 1, 2); + } + + @Test + public void testNodeLevelScreens() { + Map screens = + Map.of( + "", By.id("index"), + "~logging", By.id("logging"), + "~logging/level", By.id("logging"), + "~cores", By.id("cores"), + "~collections", By.id("collections"), + "~java-properties", By.id("java-properties"), + "~threads", By.id("threads"), + "~security", By.id("securityPanel"), + "login", By.id("login")); + screens.forEach(this::smoke); + } + + @Test + public void testCloudScreens() { + Map screens = + Map.of( + "~cloud?view=nodes", By.id("nodes-content"), + "~cloud?view=tree", By.id("tree-content"), + "~cloud?view=zkstatus", By.id("zk-status-content"), + "~cloud?view=graph", By.id("graph-content")); + screens.forEach(this::smoke); + } + + @Test + public void testSchemaDesignerScreen() { + smoke("~schema-designer", By.id("designer")); + } + + @Test + public void testCollectionScreens() { + Map screens = + Map.of( + COLLECTION + "/collection-overview", By.id("dashboard"), + COLLECTION + "/analysis", By.id("analysis"), + COLLECTION + "/documents", By.id("documents"), + COLLECTION + "/files", By.id("files"), + COLLECTION + "/query", By.id("query"), + COLLECTION + "/stream", By.id("stream"), + COLLECTION + "/paramsets", By.id("paramsets"), + COLLECTION + "/schema", By.id("schema")); + screens.forEach(this::smoke); + } + + @Test + public void testCoreScreens() { + // Plugins and Segments are core-level screens: their menu links use the core name + String coreName = + cluster.getJettySolrRunner(0).getCoreContainer().getAllCoreNames().iterator().next(); + Map screens = + Map.of( + coreName + "/plugins", By.id("plugins"), + coreName + "/segments", By.id("segments")); + screens.forEach(this::smoke); + // the ping widget on the overview answers 503 when no healthcheck file is configured, + // as is the case for the _default configset + smoke(coreName + "/core-overview", By.id("dashboard"), "/admin/ping"); + } + + private void smoke(String route, By anchor) { + smoke(route, anchor, new String[0]); + } + + private void smoke(String route, By anchor, String... allowedConsoleErrors) { + try { + openPage(route, anchor); + assertNoSevereConsoleErrors(allowedConsoleErrors); + } catch (AssertionError | RuntimeException e) { + throw new AssertionError("Screen '" + route + "' failed to render: " + e.getMessage(), e); + } + } +} From 6f5e108ea032c06d22f497228670a4d86a5729bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:39:28 +0200 Subject: [PATCH 05/30] Upgrade byte-buddy to 1.18.11 project-wide Selenium requires 1.18.11 on the webapp test classpath; align the shared version so only a single byte-buddy version remains in solr/licenses. --- gradle/libs.versions.toml | 2 +- solr/core/gradle.lockfile | 2 +- solr/cross-dc-manager/gradle.lockfile | 4 ++-- solr/licenses/byte-buddy-1.18.9.jar.sha1 | 1 - solr/licenses/byte-buddy-agent-1.18.11.jar.sha1 | 1 + solr/licenses/byte-buddy-agent-1.18.9.jar.sha1 | 1 - solr/modules/analysis-extras/gradle.lockfile | 2 +- solr/modules/cross-dc/gradle.lockfile | 4 ++-- solr/modules/jwt-auth/gradle.lockfile | 2 +- solr/modules/ltr/gradle.lockfile | 2 +- solr/modules/s3-repository/gradle.lockfile | 2 +- solr/solrj/gradle.lockfile | 2 +- 12 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 solr/licenses/byte-buddy-1.18.9.jar.sha1 create mode 100644 solr/licenses/byte-buddy-agent-1.18.11.jar.sha1 delete mode 100644 solr/licenses/byte-buddy-agent-1.18.9.jar.sha1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8b039b474ec..906c625c2950 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ benmanes-versions = "0.54.0" bouncycastle = "1.84" # @keep Browserify version used in ref-guide browserify = "17.0.0" -bytebuddy = "1.18.9" +bytebuddy = "1.18.11" carrot2-core = "4.8.6" carrotsearch-hppc = "0.10.0" carrotsearch-randomizedtesting = "2.9.1" diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index ddfcd99d6046..9e948f2a9e85 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -84,7 +84,7 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=compileClasspath,compileClasspathCopy,jarV jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy org.apache.commons:commons-exec:1.6.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.apache.commons:commons-lang3:3.20.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy diff --git a/solr/cross-dc-manager/gradle.lockfile b/solr/cross-dc-manager/gradle.lockfile index a60d7f62fdd8..0893d8e825c6 100644 --- a/solr/cross-dc-manager/gradle.lockfile +++ b/solr/cross-dc-manager/gradle.lockfile @@ -100,8 +100,8 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,runtimeLibs jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.18.9=jarValidation,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.11=jarValidation,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testRuntimeClasspath net.sf.jopt-simple:jopt-simple:5.0.4=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath net.sourceforge.argparse4j:argparse4j:0.7.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/licenses/byte-buddy-1.18.9.jar.sha1 b/solr/licenses/byte-buddy-1.18.9.jar.sha1 deleted file mode 100644 index 094aa7bb39bd..000000000000 --- a/solr/licenses/byte-buddy-1.18.9.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -70ba178486bc4d539fa63364ffa30d6b96015cff diff --git a/solr/licenses/byte-buddy-agent-1.18.11.jar.sha1 b/solr/licenses/byte-buddy-agent-1.18.11.jar.sha1 new file mode 100644 index 000000000000..f5c15c8124c7 --- /dev/null +++ b/solr/licenses/byte-buddy-agent-1.18.11.jar.sha1 @@ -0,0 +1 @@ +82212e5b3633e7a65fca8bec525762cfb6c9bae4 diff --git a/solr/licenses/byte-buddy-agent-1.18.9.jar.sha1 b/solr/licenses/byte-buddy-agent-1.18.9.jar.sha1 deleted file mode 100644 index f06d122b97db..000000000000 --- a/solr/licenses/byte-buddy-agent-1.18.9.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0f0a6f61528233035d7fd167e5ed3a711dfd0ebb diff --git a/solr/modules/analysis-extras/gradle.lockfile b/solr/modules/analysis-extras/gradle.lockfile index 87c45ec45920..ff0665d90d76 100644 --- a/solr/modules/analysis-extras/gradle.lockfile +++ b/solr/modules/analysis-extras/gradle.lockfile @@ -80,7 +80,7 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,runtimeLibs jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/modules/cross-dc/gradle.lockfile b/solr/modules/cross-dc/gradle.lockfile index 0044e890b0b1..833e59e730bf 100644 --- a/solr/modules/cross-dc/gradle.lockfile +++ b/solr/modules/cross-dc/gradle.lockfile @@ -80,8 +80,8 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,runtimeLibs jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.18.9=jarValidation,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.11=jarValidation,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/modules/jwt-auth/gradle.lockfile b/solr/modules/jwt-auth/gradle.lockfile index b23540782e98..9c67ba29783e 100644 --- a/solr/modules/jwt-auth/gradle.lockfile +++ b/solr/modules/jwt-auth/gradle.lockfile @@ -94,7 +94,7 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,runtimeLibs jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath net.minidev:accessors-smart:2.5.2=jarValidation,testCompileClasspath,testRuntimeClasspath net.minidev:json-smart:2.5.2=jarValidation,testCompileClasspath,testRuntimeClasspath no.nav.security:mock-oauth2-server:5.0.1=jarValidation,testCompileClasspath,testRuntimeClasspath diff --git a/solr/modules/ltr/gradle.lockfile b/solr/modules/ltr/gradle.lockfile index 83645788f9d9..31e71c8985d4 100644 --- a/solr/modules/ltr/gradle.lockfile +++ b/solr/modules/ltr/gradle.lockfile @@ -78,7 +78,7 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,runtimeLibs jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/modules/s3-repository/gradle.lockfile b/solr/modules/s3-repository/gradle.lockfile index e954715ad597..b823fedae1b6 100644 --- a/solr/modules/s3-repository/gradle.lockfile +++ b/solr/modules/s3-repository/gradle.lockfile @@ -92,7 +92,7 @@ jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runti javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor joda-time:joda-time:2.14.2=jarValidation,testCompileClasspath,testRuntimeClasspath junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/solrj/gradle.lockfile b/solr/solrj/gradle.lockfile index 3336889d484a..5715d8d99999 100644 --- a/solr/solrj/gradle.lockfile +++ b/solr/solrj/gradle.lockfile @@ -78,7 +78,7 @@ jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,runtimeClasspath,testRuntime jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.9=jarValidation,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,testRuntimeClasspath From ae76824f0b9403a16b666b15b828e5552584ead8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:46:15 +0200 Subject: [PATCH 06/30] Admin UI tests: node-level screen display assertions Java properties, thread dump, logging tree, cloud nodes/tree views, collections detail, core admin, security and login screens, verified against the corresponding admin APIs. Serves a minimal stub of the generated js-client bundle so the Collections screen's CollectionsV2 service instantiates in tests. --- solr/webapp/build.gradle | 2 + solr/webapp/gradle.lockfile | 10 +- .../solr/webapp/AdminUiNodeScreensTest.java | 135 ++++++++++++++++++ .../apache/solr/webapp/AdminUiTestBase.java | 91 +++++++++++- 4 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle index 5430dbaca3cf..eea5e76b8a92 100644 --- a/solr/webapp/build.gradle +++ b/solr/webapp/build.gradle @@ -55,6 +55,8 @@ dependencies { testImplementation project(':solr:solrj') testImplementation project(':solr:test-framework') testImplementation libs.carrotsearch.randomizedtesting.runner + testImplementation libs.eclipse.jetty.ee10.servlet + testImplementation libs.jakarta.servlet.api testImplementation libs.junit.junit testImplementation(libs.selenium.chromedriver, { // solr-core already provides the opentelemetry api/sdk that selenium's optional diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile index 63a691f47c3c..fe5a4e3e3206 100644 --- a/solr/webapp/gradle.lockfile +++ b/solr/webapp/gradle.lockfile @@ -72,7 +72,7 @@ io.swagger.core.v3:swagger-annotations-jakarta:2.2.52=jarValidation,solrCore,tes jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,solrCore,testRuntimeClasspath jakarta.annotation:jakarta.annotation-api:3.0.0=jarValidation,solrCore,testRuntimeClasspath jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,solrCore,testRuntimeClasspath -jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,serverLib,solrCore,testRuntimeClasspath +jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.1.0=jarValidation,solrCore,testRuntimeClasspath jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,solrCore,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,solrCore,testRuntimeClasspath @@ -120,7 +120,7 @@ org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath org.codehaus.woodstox:stax2-api:4.3.0=jarValidation,solrCore,testRuntimeClasspath org.eclipse.jetty.compression:jetty-compression-common:12.1.10=jarValidation,solrCore,testRuntimeClasspath org.eclipse.jetty.compression:jetty-compression-gzip:12.1.10=jarValidation,solrCore,testRuntimeClasspath -org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.10=jarValidation,serverLib,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlets:12.1.10=serverLib org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.10=serverLib org.eclipse.jetty.ee:jetty-ee-webapp:12.1.10=serverLib @@ -139,9 +139,9 @@ org.eclipse.jetty:jetty-http:12.1.10=jarValidation,serverLib,solrCore,testCompil org.eclipse.jetty:jetty-io:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-jmx:12.1.10=serverLib org.eclipse.jetty:jetty-rewrite:12.1.10=jarValidation,serverLib,testRuntimeClasspath -org.eclipse.jetty:jetty-security:12.1.10=jarValidation,serverLib,solrCore,testRuntimeClasspath -org.eclipse.jetty:jetty-server:12.1.10=jarValidation,serverLib,solrCore,testRuntimeClasspath -org.eclipse.jetty:jetty-session:12.1.10=jarValidation,serverLib,testRuntimeClasspath +org.eclipse.jetty:jetty-security:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-server:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-session:12.1.10=jarValidation,serverLib,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-util:12.1.10=jarValidation,serverLib,solrCore,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-xml:12.1.10=serverLib org.glassfish.hk2.external:aopalliance-repackaged:4.0.1=jarValidation,solrCore,testRuntimeClasspath diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java new file mode 100644 index 000000000000..c6c214c95c65 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java @@ -0,0 +1,135 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.util.ExternalPaths; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Verifies the data displayed on the node-level Admin UI screens against the backing APIs. */ +public class AdminUiNodeScreensTest extends AdminUiTestBase { + + private static final String COLLECTION = "nodescoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); + CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 2) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(COLLECTION, 1, 2); + } + + @Test + public void testJavaPropertiesMatchApi() throws Exception { + NamedList response = adminApi("/admin/info/properties", params()); + Map props = (Map) response.get("system.properties"); + String expectedJavaVersion = (String) props.get("java.version"); + + openPage("~java-properties", By.id("java-properties")); + waitFor(By.cssSelector("#java-properties li")); + // find the row for java.version and compare its value; the UI inserts zero-width + // spaces (​) into names and values for line wrapping, so strip them + String value = null; + for (WebElement row : driver.findElements(By.cssSelector("#java-properties li"))) { + String name = row.findElement(By.cssSelector("dt")).getText().replace("\u200B", ""); + if (name.equals("java.version")) { + value = row.findElement(By.cssSelector("dd")).getText().replace("\u200B", ""); + } + } + assertEquals(expectedJavaVersion, value); + } + + @Test + public void testThreadDumpShowsThreads() { + openPage("~threads", By.id("thread-dump")); + List rows = driver.findElements(By.cssSelector("#thread-dump tbody tr")); + assertFalse("Thread dump should list threads", rows.isEmpty()); + // a Jetty worker thread is always present in a running Solr node + waitForPageContains("qtp"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testLoggingLevelTree() { + openPage("~logging/level", By.id("loggingtree")); + waitForPageContains("org.apache.solr"); + // the level legend/menu offers the standard levels + waitFor(By.cssSelector("#loggingtree .jstree-anchor")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCloudNodesListsAllNodes() { + openPage("~cloud?view=nodes", By.id("nodes-table")); + // both cluster nodes run on the same host, shown as two node rows + waitForPageContains("Hosts 1 - 1 of 1"); + List nodeNames = driver.findElements(By.cssSelector("#nodes-table .node-name")); + assertEquals("Expected one row per live node", 2, nodeNames.size()); + for (var jetty : cluster.getJettySolrRunners()) { + waitForPageContains(":" + jetty.getLocalPort()); + } + assertNoSevereConsoleErrors(); + } + + @Test + public void testCloudTreeShowsZkNodes() { + openPage("~cloud?view=tree", By.id("tree-content")); + waitForPageContains("live_nodes"); + waitForPageContains("collections"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCollectionsScreenShowsCollectionDetail() { + openPage("~collections/" + COLLECTION, By.id("collections")); + waitForPageContains(COLLECTION); + waitForPageContains("shard1"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCoreAdminShowsCore() throws Exception { + NamedList response = adminApi("/admin/cores", params()); + Map status = (Map) response.get("status"); + assertFalse("Node should host at least one core", status.isEmpty()); + String coreName = status.keySet().iterator().next().toString(); + + openPage("~cores", By.id("cores")); + waitForPageContains(coreName); + assertNoSevereConsoleErrors(); + } + + @Test + public void testSecurityScreenWarnsNotEnabled() { + openPage("~security", By.id("securityPanel")); + waitForPageContains("Security is not enabled"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testLoginScreenWithoutAuthentication() { + openPage("login", By.id("login")); + waitForPageContains("uthentication"); + assertNoSevereConsoleErrors(); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index 40bd792a7146..afb7d1c33046 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -19,6 +19,9 @@ import com.carrotsearch.randomizedtesting.ThreadFilter; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.nio.file.Files; @@ -36,6 +39,7 @@ import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.Assume; import org.junit.BeforeClass; @@ -93,6 +97,35 @@ public abstract class AdminUiTestBase extends SolrCloudTestCase { /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ protected static String baseUrl; + /** + * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), + * which only exists inside the built webapp, not in the source tree tests serve from. The + * AngularJS {@code CollectionsV2} service fails to instantiate without the {@code solrApi} + * global, taking the whole Collections screen down with it. Only the small API surface the + * AngularJS UI actually uses is stubbed. + */ + public static class StubJsClientServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/javascript"); + resp.getWriter() + .write( + "var solrApi = {\n" + + " ApiClient: { instance: { basePath: '/api', defaultHeaders: {} } },\n" + + " CollectionsApi: function() {\n" + + " this.reloadCollection = function(name, callback) {\n" + + " var xhr = new XMLHttpRequest();\n" + + " xhr.open('POST', '/api/collections/' + name + '/reload');\n" + + " xhr.setRequestHeader('Content-Type', 'application/json');\n" + + " xhr.onload = function() { callback(null, null, {status: xhr.status}); };\n" + + " xhr.onerror = function() { callback(new Error('reload failed'), null, {status: xhr.status}); };\n" + + " xhr.send('{}');\n" + + " };\n" + + " }\n" + + "};\n"); + } + } + /** Ignores threads spawned by Selenium and the JDK http client it uses. */ public static class WebDriverThreadsFilter implements ThreadFilter { @Override @@ -120,7 +153,15 @@ public static void startClusterAndBrowser() throws Exception { // metrics are off by default in test clusters, but UI screens (e.g. Plugins) need them; // restored after the class by SolrTestCase's SystemPropertiesRestoreRule System.setProperty("metricsEnabled", "true"); - configureCluster(2).withJettyConfig(jetty -> jetty.enableAdminUi(true)).configure(); + configureCluster(2) + .withJettyConfig( + jetty -> + jetty + .enableAdminUi(true) + // exact-path mapping takes precedence over the static /libs/* servlet + .withServlet( + new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js")) + .configure(); baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); ChromeOptions options = new ChromeOptions(); @@ -244,6 +285,49 @@ protected static NamedList adminApi(String path, SolrParams params) } } + /** Waits until the page source contains the given text. */ + protected static void waitForPageContains(String text) { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + while (System.nanoTime() < deadlineNanos) { + if (driver.getPageSource().contains(text)) { + return; + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new AssertionError("Timed out waiting for page to contain: " + text); + } + + /** + * Selects an option in a "chosen"-decorated select element. The original select is hidden by the + * widget, so this drives the generated container instead. + */ + protected static void chosenSelect(String selectId, String optionText) { + WebElement container = waitFor(By.id(selectId + "_chosen")); + container.click(); + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + while (System.nanoTime() < deadlineNanos) { + for (WebElement option : + container.findElements(By.cssSelector(".chosen-results li.active-result"))) { + if (optionText.equals(option.getText())) { + option.click(); + return; + } + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new AssertionError("Option '" + optionText + "' not found in select " + selectId); + } + /** * Fails the test if the browser console contains SEVERE errors, ignoring known-benign ones and * any messages containing one of {@code allowedSubstrings}. @@ -258,11 +342,6 @@ protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { java.util.Arrays.stream(allowedSubstrings) .noneMatch(allowed -> entry.getMessage().contains(allowed))) .filter(entry -> !entry.getMessage().contains("favicon.ico")) - // the js-client bundle is generated into the war at build time and does not - // exist in the source tree that tests serve from; the UI degrades gracefully - .filter(entry -> !entry.getMessage().contains("libs/solr/index.js")) - // "solrApi" is defined by that same missing js-client bundle - .filter(entry -> !entry.getMessage().contains("solrApi is not defined")) .toList(); assertTrue("Severe browser console errors: " + severe, severe.isEmpty()); } From 565a3c878791359379ffef08c90bb5ea4e781187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:55:42 +0200 Subject: [PATCH 07/30] Admin UI tests: per-collection screens and write actions Collection screens: query execution, analysis, schema browser, files, segments, plugins, documents form, paramsets, overview. Write actions through the UI: create and delete a collection, index a document, change and revert a log level - all verified via the corresponding APIs. --- dev-docs/admin-ui-tests.md | 85 +++++---- .../webapp/AdminUiCollectionScreensTest.java | 175 +++++++++++++++++ .../solr/webapp/AdminUiNodeScreensTest.java | 1 + .../apache/solr/webapp/AdminUiTestBase.java | 11 ++ .../solr/webapp/AdminUiWriteActionsTest.java | 177 ++++++++++++++++++ 5 files changed, 413 insertions(+), 36 deletions(-) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md index be27a586e14c..8a68d5c91daf 100644 --- a/dev-docs/admin-ui-tests.md +++ b/dev-docs/admin-ui-tests.md @@ -38,13 +38,13 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI Navigate every route, wait for a screen-specific anchor element, assert no severe browser console errors. -- [ ] Node-level routes: `/`, `~logging`, `~logging/level`, `~cloud?view=nodes`, +- [x] Node-level routes: `/`, `~logging`, `~logging/level`, `~cloud?view=nodes`, `~cloud?view=tree`, `~cloud?view=zkstatus`, `~cloud?view=graph`, `~cores`, `~collections`, `~schema-designer`, `~security`, `~java-properties`, `~threads`, `login` -- [ ] Per-collection routes (fixture collection): `collection-overview`, +- [x] Per-collection routes (fixture collection): `collection-overview`, `analysis`, `documents`, `files`, `query`, `stream`, `paramsets`, - `plugins`, `schema`, `segments` + `schema`; per-core routes: `core-overview`, `plugins`, `segments` Flaky-risk flags: `~cloud?view=graph` (d3 svg async), `~cloud?view=zkstatus` (ZK admin-command availability in the embedded ensemble), `~schema-designer` @@ -54,42 +54,49 @@ Flaky-risk flags: `~cloud?view=graph` (d3 svg async), `~cloud?view=zkstatus` - [x] Dashboard (`AdminUiDashboardTest`): versions, JVM info, memory bars, security warning vs `/admin/info/system` -- [ ] Java Properties: a few props from `/admin/info/properties` rendered -- [ ] Thread Dump: thread list non-empty, known thread name, expand stacktrace -- [ ] Logging: logger tree renders, `org.apache.solr` row with level -- [ ] Cloud > Nodes: both nodes listed, host:port match cluster -- [ ] Cloud > Tree: `/live_nodes` count matches, expand collection `state.json` -- [ ] Cloud > ZK Status: ensemble status shown (lenient assertions) -- [ ] Cloud > Graph: collection node and replica leaves in SVG (lenient) -- [ ] Collections: created collection listed; detail shows shards/replicas Active -- [ ] Core Admin: core selector lists the core, overview matches `/admin/cores` -- [ ] Security: "security is not enabled" warning panel (no auth configured) -- [ ] Login: not-authenticated info page when no authenticationPlugin +- [x] Java Properties: `java.version` value matches `/admin/info/properties` + (`AdminUiNodeScreensTest`) +- [x] Thread Dump: thread list non-empty, Jetty worker thread shown +- [x] Logging: logger tree renders with `org.apache.solr` row +- [x] Cloud > Nodes: one row per live node, ports match the cluster +- [x] Cloud > Tree: `live_nodes` and `collections` znodes shown +- [x] Cloud > ZK Status / Graph: render without console errors (smoke only) +- [x] Collections: collection listed; detail shows shard info +- [x] Core Admin: hosted core name shown, matching `/admin/cores` +- [x] Security: "security is not enabled" warning panel (no auth configured) +- [x] Login: authentication info page shown when no authenticationPlugin +- [ ] Deeper assertions: cloud graph replica leaves, ZK status ensemble + details, logging events viewer content ## Phase 3 — Per-collection screens (fixture: collection with pre-indexed docs) -- [ ] Collection Overview: numDocs/maxDoc match API, healthy replica badge -- [ ] Query: run `*:*` via form, response block shows expected `numFound`; - change `rows` and re-run -- [ ] Analysis: analyze a value for `text_general`, token table lowercases -- [ ] Documents (display): form renders, doc-type dropdown options present -- [ ] Schema Browser: field list contains `id`, flags match `/schema` API, - term info loads for a populated field -- [ ] Files: tree lists `solrconfig.xml`, content loads -- [ ] Plugins/Stats: categories listed, searcher stats show numDocs -- [ ] Segments: segment bars present after commit -- [ ] Paramsets (display): empty-state or created paramset shown -- [ ] Stream: simple streaming expression executes and renders result (medium risk) -- [ ] Replication in cloud mode: verify what the screen shows; standalone-mode - coverage deferred +Covered by `AdminUiCollectionScreensTest`: + +- [x] Collection Overview: shard info shown +- [x] Query: `*:*` finds all fixture docs, `id:` query finds exactly one +- [x] Analysis: `text_general` tokenizes and lowercases entered text +- [x] Documents (display): doc-type dropdown offers JSON/XML/CSV, submit present +- [x] Schema Browser: editable-schema action buttons, `_version_` field listed +- [x] Files: tree lists `solrconfig.xml`, file content renders +- [x] Plugins/Stats: searcher stats present (needs `metricsEnabled=true`) +- [x] Segments: at least one segment rendered after commit +- [x] Paramsets (display): form renders +- [ ] Query: paramsets dropdown, dismax/edismax toggles, raw query params +- [ ] Schema Browser: per-field flags vs `/schema` API, term info loading +- [ ] Stream: simple streaming expression executes and renders result +- [ ] Replication in cloud mode; standalone-mode coverage deferred ## Phase 4 — Write actions through the UI -- [ ] Collections: create collection via dialog → verify via CLUSTERSTATUS → - delete via UI → gone. Create/delete alias. Add replica. -- [ ] Documents: submit JSON doc → success response → found via UI Query and SolrJ -- [ ] Schema Browser: add field → verify in UI and `/schema/fields` → delete field -- [ ] Logging: set a logger to WARN via level editor → verify via API → revert +Covered by `AdminUiWriteActionsTest`: + +- [x] Collections: create collection via dialog → verify via API → delete via + UI with typed confirmation → gone +- [x] Documents: submit JSON doc via form → success response → searchable +- [x] Logging: set logger to WARN via level editor → verify via API → revert + to unset +- [ ] Collections: create/delete alias, add/delete replica, reload +- [ ] Schema Browser: add field → verify via `/schema/fields` → delete field - [ ] Core Admin: RELOAD core via UI (rename/swap/unload deferred to a standalone-mode class) - [ ] Paramsets: create paramset via UI → verify via `/config/params` @@ -103,9 +110,15 @@ Policy: phases 1–3 run in the default test run; heavyweight phase-4 classes ## Known limitations - The generated js-client bundle (`libs/solr/index.js`) only exists inside the - built WAR, not in the source tree tests serve from; its 404 is whitelisted in - the console-error assertion and v2-API-backed UI features relying on it are - not exercised. + built WAR, not in the source tree tests serve from. `AdminUiTestBase` serves + a minimal stub defining the `solrApi` global (only `reloadCollection` is used + by the AngularJS UI) so the Collections screen works; a future improvement + could serve the real bundle when it has been built. +- The shared menu code intermittently logs a benign + `TypeError: Cannot read properties of null (reading 'name')` while the + per-collection menu resolves; allowed in the paramsets test. +- The core overview ping widget answers 503 when the configset has no + healthcheck file; allowed in the smoke test. - ASF Jenkins has no Chrome, so these tests skip there; they run on developer machines and could run in a GitHub Actions workflow (Chrome preinstalled on `ubuntu-latest`) as a follow-up. diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java new file mode 100644 index 000000000000..b2a8bd5e7940 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java @@ -0,0 +1,175 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.util.ExternalPaths; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Verifies the per-collection Admin UI screens against a fixture collection with indexed documents. + */ +public class AdminUiCollectionScreensTest extends AdminUiTestBase { + + private static final String COLLECTION = "books"; + private static final int NUM_DOCS = 4; + + @BeforeClass + public static void setupCollection() throws Exception { + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); + // pin the replica to the node the browser talks to, so core-level screens + // (plugins, segments) find it locally + CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 1) + .setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(COLLECTION, 1, 1); + + SolrClient client = cluster.getSolrClient(COLLECTION); + for (int i = 1; i <= NUM_DOCS; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", Integer.toString(i)); + doc.addField("title_txt", "Book number " + i); + client.add(doc); + } + client.commit(); + } + + @Test + public void testQueryScreenExecutesQueries() { + openPage(COLLECTION + "/query", By.id("query")); + + // default *:* query finds all documents + waitFor(By.cssSelector("#query button[type=submit]")).click(); + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); + + // a specific id query finds exactly one document + WebElement queryInput = waitFor(By.id("q")); + queryInput.clear(); + queryInput.sendKeys("id:1"); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":1"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testAnalysisScreenAnalyzesText() { + openPage(COLLECTION + "/analysis", By.id("analysis-holder")); + chosenSelect("type_or_name", "text_general"); + WebElement indexText = waitFor(By.id("analysis_fieldvalue_index")); + indexText.clear(); + indexText.sendKeys("Running QUICKLY"); + waitFor(By.cssSelector("#field-analysis button[type=submit]")).click(); + // text_general tokenizes and lowercases + waitForPageContains("running"); + waitForPageContains("quickly"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testSchemaScreenShowsFields() { + openPage(COLLECTION + "/schema", By.id("schema")); + // managed schema is editable, so the action buttons are shown + waitFor(By.id("addField")); + // known fields from the _default configset are browsable + waitForPageContains("_version_"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testFilesScreenShowsConfig() { + openPage(COLLECTION + "/files", By.id("files")); + waitForPageContains("solrconfig.xml"); + // open the file and check its content is rendered + openPage(COLLECTION + "/files?file=solrconfig.xml", By.id("files")); + waitForPageContains("luceneMatchVersion"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testSegmentsScreenShowsSegments() throws Exception { + String coreName = coreNameOnNode0(); + openPage(coreName + "/segments", By.id("segments")); + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + List segments = List.of(); + while (System.nanoTime() < deadlineNanos) { + segments = driver.findElements(By.cssSelector("#segments #response li")); + if (!segments.isEmpty()) break; + Thread.sleep(200); + } + assertFalse("Expected at least one segment after committing docs", segments.isEmpty()); + assertNoSevereConsoleErrors(); + } + + @Test + public void testPluginsScreenShowsStats() throws Exception { + String coreName = coreNameOnNode0(); + openPage(coreName + "/plugins", By.id("plugins")); + waitForPageContains("searcher"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testDocumentsScreenForm() { + openPage(COLLECTION + "/documents", By.id("documents")); + List types = + driver.findElements(By.cssSelector("#document-type option")).stream() + .map(WebElement::getText) + .collect(Collectors.toList()); + assertTrue("Doc type dropdown should offer JSON, got " + types, types.contains("JSON")); + assertTrue("Doc type dropdown should offer XML, got " + types, types.contains("XML")); + assertTrue("Doc type dropdown should offer CSV, got " + types, types.contains("CSV")); + waitFor(By.id("submit")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testParamsetsScreenRenders() { + openPage(COLLECTION + "/paramsets", By.id("paramsets")); + waitFor(By.cssSelector("#paramsets #form")); + // the shared menu code intermittently throws a benign TypeError while the + // per-collection menu resolves; the screen itself renders fine + assertNoSevereConsoleErrors("Cannot read properties of null (reading 'name')"); + } + + @Test + public void testCollectionOverviewShowsShard() { + openPage(COLLECTION + "/collection-overview", By.id("dashboard")); + waitForPageContains("shard1"); + assertNoSevereConsoleErrors(); + } + + /** Returns the fixture collection's core name on node 0, the node the browser talks to. */ + private static String coreNameOnNode0() { + for (String name : cluster.getJettySolrRunner(0).getCoreContainer().getAllCoreNames()) { + if (name.startsWith(COLLECTION + "_")) { + return name; + } + } + throw new AssertionError("No core found on node 0 for collection " + COLLECTION); + } + + private static String abbreviate(String s) { + return s.length() > 300 ? s.substring(0, 300) + "..." : s; + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java index c6c214c95c65..ab10ee6808e0 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java @@ -62,6 +62,7 @@ public void testJavaPropertiesMatchApi() throws Exception { @Test public void testThreadDumpShowsThreads() { openPage("~threads", By.id("thread-dump")); + waitFor(By.cssSelector("#thread-dump tbody tr")); List rows = driver.findElements(By.cssSelector("#thread-dump tbody tr")); assertFalse("Thread dump should list threads", rows.isEmpty()); // a Jetty worker thread is always present in a running Solr node diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index afb7d1c33046..b49326765667 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -285,6 +285,17 @@ protected static NamedList adminApi(String path, SolrParams params) } } + /** Waits until the element's rendered text contains the given substring, and returns it. */ + protected static String waitForTextContains(By locator, String substring) { + return poll( + locator, + el -> { + String text = el.getText(); + return text.contains(substring) ? text : null; + }, + "text containing '" + substring + "'"); + } + /** Waits until the page source contains the given text. */ protected static void waitForPageContains(String text) { long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java new file mode 100644 index 000000000000..44b23b7dda73 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java @@ -0,0 +1,177 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.SolrQuery; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.util.ExternalPaths; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Exercises write actions performed through the Admin UI, verifying the effect via the APIs. */ +public class AdminUiWriteActionsTest extends AdminUiTestBase { + + private static final String CONFIG = "writeconf"; + private static final String COLLECTION = "writecoll"; + + @BeforeClass + public static void setupFixture() throws Exception { + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, CONFIG); + CollectionAdminRequest.createCollection(COLLECTION, CONFIG, 1, 1) + .setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(COLLECTION, 1, 1); + } + + @Test + public void testCreateAndDeleteCollectionViaUi() throws Exception { + String name = "uicreated"; + openPage("~collections", By.id("collections")); + + // create through the Add Collection dialog + waitFor(By.cssSelector("#navigation button#add")).click(); + WebElement nameInput = waitFor(By.id("add_name")); + nameInput.clear(); + nameInput.sendKeys(name); + chosenSelect("add_config", CONFIG); + WebElement numShards = waitFor(By.id("add_numShards")); + numShards.clear(); + numShards.sendKeys("1"); + WebElement replicationFactor = waitFor(By.id("add_replicationFactor")); + replicationFactor.clear(); + replicationFactor.sendKeys("1"); + waitFor(By.xpath("//button[@ng-click='addCollection()']")).click(); + + // the new collection shows up in the list, and the API confirms it + waitForPageContains(name); + assertCollectionExists(name, true); + + // delete it through the delete dialog, which requires typing the name to confirm + openPage("~collections/" + name, By.id("collections")); + waitFor(By.id("delete-collection")).click(); + WebElement confirmInput = waitFor(By.id("collectionDeleteConfirm")); + confirmInput.clear(); + confirmInput.sendKeys(name); + waitFor(By.xpath("//button[@ng-click='deleteCollection()']")).click(); + + assertCollectionExists(name, false); + assertNoSevereConsoleErrors(); + } + + @Test + public void testIndexDocumentViaUi() throws Exception { + openPage(COLLECTION + "/documents", By.id("documents")); + WebElement docInput = waitFor(By.id("document")); + docInput.clear(); + docInput.sendKeys("{\"id\":\"ui-doc-1\",\"title_txt\":\"indexed from the admin ui\"}"); + waitFor(By.id("submit")).click(); + waitForTextContains(By.cssSelector("#documents #result"), "success"); + + // the document becomes searchable (the form defaults to commitWithin=1000) + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + long numFound = 0; + while (System.nanoTime() < deadlineNanos) { + numFound = + cluster + .getSolrClient(COLLECTION) + .query(new SolrQuery("id:ui-doc-1")) + .getResults() + .getNumFound(); + if (numFound > 0) break; + Thread.sleep(250); + } + assertEquals("Document indexed via UI should be searchable", 1, numFound); + assertNoSevereConsoleErrors(); + } + + @Test + public void testChangeLogLevelViaUi() throws Exception { + String logger = "org.apache.solr.core"; + openPage("~logging/level", By.id("loggingtree")); + + WebElement anchor = + waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")); + anchor.click(); + waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='WARN']")).click(); + assertLoggerLevel(logger, "WARN"); + + // revert to unset; the logger then reports the inherited level with set=false + waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")).click(); + waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='UNSET']")).click(); + assertLoggerLevel(logger, null); + assertNoSevereConsoleErrors(); + } + + private void assertCollectionExists(String name, boolean expectExists) throws Exception { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + boolean exists = !expectExists; + while (System.nanoTime() < deadlineNanos) { + List collections = CollectionAdminRequest.listCollections(cluster.getSolrClient()); + exists = collections.contains(name); + if (exists == expectExists) return; + Thread.sleep(250); + } + fail( + "Collection " + + name + + " should " + + (expectExists ? "" : "not ") + + "exist, but does" + + (exists ? "" : " not")); + } + + /** + * Asserts the level a logger was explicitly set to, or with {@code expectedLevel} null, that the + * logger has no explicit level (it then reports the inherited effective level with set=false). + */ + @SuppressWarnings("unchecked") + private void assertLoggerLevel(String logger, String expectedLevel) throws Exception { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + Object actualLevel = "(logger not found)"; + Object actualSet = null; + while (System.nanoTime() < deadlineNanos) { + NamedList response = adminApi("/admin/info/logging", params()); + for (Map entry : (List>) response.get("loggers")) { + if (logger.equals(entry.get("name"))) { + actualLevel = entry.get("level"); + actualSet = entry.get("set"); + } + } + boolean matches = + expectedLevel == null + ? Boolean.FALSE.equals(actualSet) + : expectedLevel.equals(actualLevel) && Boolean.TRUE.equals(actualSet); + if (matches) return; + Thread.sleep(250); + } + fail( + "Logger " + + logger + + " expected level " + + (expectedLevel == null ? "(unset)" : expectedLevel) + + " but was " + + actualLevel + + " (set=" + + actualSet + + ")"); + } +} From 93d56991eb34428b4974f0447465cecf419f17fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 13 Aug 2026 23:56:23 +0200 Subject: [PATCH 08/30] Add changelog entry --- changelog/unreleased/admin-ui-selenium-tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/unreleased/admin-ui-selenium-tests.yml diff --git a/changelog/unreleased/admin-ui-selenium-tests.yml b/changelog/unreleased/admin-ui-selenium-tests.yml new file mode 100644 index 000000000000..c948e23b903f --- /dev/null +++ b/changelog/unreleased/admin-ui-selenium-tests.yml @@ -0,0 +1,8 @@ +title: > + Added Selenium-based JUnit test coverage for the Admin UI, driving a headless Chrome against a test cluster. +type: other +authors: + - name: Jan Høydahl +links: + - name: SOLR-8474 + url: https://issues.apache.org/jira/browse/SOLR-8474 From cb78e4fc28e70ab234d469208b444aae33cb1172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:02:26 +0200 Subject: [PATCH 09/30] Fix forbidden API usage in Admin UI test harness --- .../apache/solr/webapp/AdminUiTestBase.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index b49326765667..aeb5aa70153c 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -108,21 +108,22 @@ public static class StubJsClientServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/javascript"); - resp.getWriter() + resp.getOutputStream() .write( - "var solrApi = {\n" - + " ApiClient: { instance: { basePath: '/api', defaultHeaders: {} } },\n" - + " CollectionsApi: function() {\n" - + " this.reloadCollection = function(name, callback) {\n" - + " var xhr = new XMLHttpRequest();\n" - + " xhr.open('POST', '/api/collections/' + name + '/reload');\n" - + " xhr.setRequestHeader('Content-Type', 'application/json');\n" - + " xhr.onload = function() { callback(null, null, {status: xhr.status}); };\n" - + " xhr.onerror = function() { callback(new Error('reload failed'), null, {status: xhr.status}); };\n" - + " xhr.send('{}');\n" - + " };\n" - + " }\n" - + "};\n"); + ("var solrApi = {\n" + + " ApiClient: { instance: { basePath: '/api', defaultHeaders: {} } },\n" + + " CollectionsApi: function() {\n" + + " this.reloadCollection = function(name, callback) {\n" + + " var xhr = new XMLHttpRequest();\n" + + " xhr.open('POST', '/api/collections/' + name + '/reload');\n" + + " xhr.setRequestHeader('Content-Type', 'application/json');\n" + + " xhr.onload = function() { callback(null, null, {status: xhr.status}); };\n" + + " xhr.onerror = function() { callback(new Error('reload failed'), null, {status: xhr.status}); };\n" + + " xhr.send('{}');\n" + + " };\n" + + " }\n" + + "};\n") + .getBytes(java.nio.charset.StandardCharsets.UTF_8)); } } @@ -144,6 +145,7 @@ public boolean reject(Thread t) { } @BeforeClass + @SuppressForbidden(reason = "Selenium's logging preferences API uses java.util.logging levels") public static void startClusterAndBrowser() throws Exception { Path chrome = findChromeBinary(); Assume.assumeTrue( @@ -343,6 +345,7 @@ protected static void chosenSelect(String selectId, String optionText) { * Fails the test if the browser console contains SEVERE errors, ignoring known-benign ones and * any messages containing one of {@code allowedSubstrings}. */ + @SuppressForbidden(reason = "Selenium's log API reports java.util.logging levels") protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { List entries = driver.manage().logs().get(LogType.BROWSER).getAll(); List severe = From d1c848dbb740f62139614ae72f7040eeb986145e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:27:46 +0200 Subject: [PATCH 10/30] Fix Error Prone UnnecessarilyFullyQualified warnings --- .../org/apache/solr/webapp/AdminUiTestBase.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index aeb5aa70153c..f7094e4904a8 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -22,12 +22,16 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.File; import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.Arrays; import java.util.List; +import java.util.function.Function; import java.util.logging.Level; import org.apache.lucene.tests.util.QuickPatchThreadsFilter; import org.apache.lucene.util.SuppressForbidden; @@ -35,6 +39,7 @@ import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.GenericSolrRequest; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.params.SolrParams; @@ -123,7 +128,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO + " };\n" + " }\n" + "};\n") - .getBytes(java.nio.charset.StandardCharsets.UTF_8)); + .getBytes(StandardCharsets.UTF_8)); } } @@ -250,8 +255,7 @@ protected static String waitForText(By locator) { * Polls the given element until {@code condition} returns non-null (a fresh lookup each round, so * elements replaced by Angular re-renders are tolerated), failing after {@link #WAIT_TIMEOUT}. */ - private static T poll( - By locator, java.util.function.Function condition, String description) { + private static T poll(By locator, Function condition, String description) { long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); WebDriverException lastException = null; while (System.nanoTime() < deadlineNanos) { @@ -281,7 +285,7 @@ private static T poll( * the UI should display. */ protected static NamedList adminApi(String path, SolrParams params) - throws IOException, org.apache.solr.client.solrj.SolrServerException { + throws IOException, SolrServerException { try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { return client.request(new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)); } @@ -353,7 +357,7 @@ protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { .filter(entry -> entry.getLevel().intValue() >= Level.SEVERE.intValue()) .filter( entry -> - java.util.Arrays.stream(allowedSubstrings) + Arrays.stream(allowedSubstrings) .noneMatch(allowed -> entry.getMessage().contains(allowed))) .filter(entry -> !entry.getMessage().contains("favicon.ico")) .toList(); @@ -391,7 +395,7 @@ protected static Path findChromeBinary() { } String pathEnv = System.getenv("PATH"); if (pathEnv != null) { - for (String dir : pathEnv.split(java.io.File.pathSeparator)) { + for (String dir : pathEnv.split(File.pathSeparator)) { for (String name : List.of("google-chrome", "chromium", "chromium-browser")) { Path path = Path.of(dir, name); if (Files.isExecutable(path)) { From 70abb58750144d711ac88e1598652df7c9a0941b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 11/30] Admin UI test harness: fixture/wait helpers, security.json hook, console log capture Adds createFixtureCollection/coreNameOnNode0/waitUntil helpers, an optional security.json for the test cluster, browser console logs in failure artifacts, a standard test log4j2 config so the log watcher sees events, and filters for two benign UI console errors. --- solr/webapp/src/test-files/log4j2.xml | 40 +++++++++ .../apache/solr/webapp/AdminUiTestBase.java | 84 +++++++++++++++++-- 2 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 solr/webapp/src/test-files/log4j2.xml diff --git a/solr/webapp/src/test-files/log4j2.xml b/solr/webapp/src/test-files/log4j2.xml new file mode 100644 index 000000000000..3e941d535daf --- /dev/null +++ b/solr/webapp/src/test-files/log4j2.xml @@ -0,0 +1,40 @@ + + + + + + + + + %maxLen{%-4r %-5p (%t) [%notEmpty{n:%X{node_name}}%notEmpty{ c:%X{collection}}%notEmpty{ s:%X{shard}}%notEmpty{ r:%X{replica}}%notEmpty{ x:%X{core}}%notEmpty{ t:%X{trace_id}}] %c{1.} %m}{10240}%n + + + + + + + + + + + + + + + + diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index f7094e4904a8..fbecd162246f 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -31,6 +31,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.logging.Level; import org.apache.lucene.tests.util.QuickPatchThreadsFilter; @@ -40,10 +41,12 @@ import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.GenericSolrRequest; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; +import org.apache.solr.util.ExternalPaths; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.Assume; @@ -102,6 +105,12 @@ public abstract class AdminUiTestBase extends SolrCloudTestCase { /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ protected static String baseUrl; + /** + * Optional security.json for the cluster. Subclasses must assign this in a {@code static} block + * (which runs before this class's cluster-starting {@code @BeforeClass} method). + */ + protected static String securityJson; + /** * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), * which only exists inside the built webapp, not in the source tree tests serve from. The @@ -144,6 +153,8 @@ public boolean reject(Thread t) { || name.startsWith("process reaper") // selenium driver-service startup checker pool, terminates on its own || name.startsWith("UrlChecker-") + // selenium's chromedriver stdout/stderr pump, stops when the process exits + || name.startsWith("External Process Output Forwarder") // JDK-internal scheduler backing CompletableFuture timeouts, lives forever || name.equals("CompletableFutureDelayScheduler"); } @@ -160,15 +171,19 @@ public static void startClusterAndBrowser() throws Exception { // metrics are off by default in test clusters, but UI screens (e.g. Plugins) need them; // restored after the class by SolrTestCase's SystemPropertiesRestoreRule System.setProperty("metricsEnabled", "true"); - configureCluster(2) - .withJettyConfig( - jetty -> - jetty - .enableAdminUi(true) - // exact-path mapping takes precedence over the static /libs/* servlet - .withServlet( - new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js")) - .configure(); + var clusterBuilder = + configureCluster(2) + .withJettyConfig( + jetty -> + jetty + .enableAdminUi(true) + // exact-path mapping takes precedence over the static /libs/* servlet + .withServlet( + new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js")); + if (securityJson != null) { + clusterBuilder.withSecurityJson(securityJson); + } + clusterBuilder.configure(); baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); ChromeOptions options = new ChromeOptions(); @@ -215,6 +230,11 @@ protected void failed(Throwable e, Description description) { byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); Files.write(dir.resolve("screenshot.png"), png); Files.writeString(dir.resolve("page.html"), driver.getPageSource()); + StringBuilder console = new StringBuilder(); + for (LogEntry entry : driver.manage().logs().get(LogType.BROWSER).getAll()) { + console.append(entry.getLevel()).append(' ').append(entry.getMessage()).append('\n'); + } + Files.writeString(dir.resolve("console.log"), console.toString()); log.error("UI test failure artifacts saved to {}", dir); } catch (Exception suppressed) { log.warn("Could not save UI failure artifacts", suppressed); @@ -291,6 +311,46 @@ protected static NamedList adminApi(String path, SolrParams params) } } + /** + * Uploads the default configset under the collection's name and creates the collection. A + * single-replica collection is pinned to the node the browser talks to, so core-level screens + * find its core locally. + */ + protected static void createFixtureCollection(String name, int numShards, int numReplicas) + throws Exception { + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, name); + CollectionAdminRequest.Create create = + CollectionAdminRequest.createCollection(name, name, numShards, numReplicas); + if (numShards * numReplicas == 1) { + create.setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()); + } + create.process(cluster.getSolrClient()); + cluster.waitForActiveCollection(name, numShards, numShards * numReplicas); + } + + /** Polls the condition until it holds, failing after {@link #WAIT_TIMEOUT}. */ + protected static void waitUntil(String description, BooleanSupplier condition) + throws InterruptedException { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + while (System.nanoTime() < deadlineNanos) { + if (condition.getAsBoolean()) { + return; + } + Thread.sleep(250); + } + fail("Timed out waiting until " + description); + } + + /** Returns the name of a core of the given collection hosted on node 0. */ + protected static String coreNameOnNode0(String collection) { + for (String name : cluster.getJettySolrRunner(0).getCoreContainer().getAllCoreNames()) { + if (name.startsWith(collection + "_")) { + return name; + } + } + throw new AssertionError("No core found on node 0 for collection " + collection); + } + /** Waits until the element's rendered text contains the given substring, and returns it. */ protected static String waitForTextContains(By locator, String substring) { return poll( @@ -360,6 +420,12 @@ protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { Arrays.stream(allowedSubstrings) .noneMatch(allowed -> entry.getMessage().contains(allowed))) .filter(entry -> !entry.getMessage().contains("favicon.ico")) + // benign race in the shared menu code: showCore() fires with a null core + // while the per-collection menu resolves after navigation + .filter( + entry -> + !(entry.getMessage().contains("reading 'name'") + && entry.getMessage().contains("showCore"))) .toList(); assertTrue("Severe browser console errors: " + severe, severe.isEmpty()); } From 8c86f7a9e112a0602b5814964c0d9a1f73cdfaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 12/30] Admin UI tests: Collections screen incl. alias, replica and reload actions Groups the collections display and write tests in one feature class: create/delete collection, create/delete alias, add/delete replica and reload, each verified through the corresponding API. --- .../webapp/AdminUiCollectionsScreenTest.java | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionsScreenTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionsScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionsScreenTest.java new file mode 100644 index 000000000000..c974a0bccab7 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionsScreenTest.java @@ -0,0 +1,203 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.response.CollectionAdminResponse; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Tests the Collections screen ({@code #/~collections}): display of collection details, and the + * write actions offered by the screen - create/delete collection, aliases, replicas and reload. + */ +public class AdminUiCollectionsScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "collscreen"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + } + + @Test + public void testCollectionDetailDisplay() { + openPage("~collections/" + COLLECTION, By.id("collections")); + waitForPageContains(COLLECTION); + waitForPageContains("shard1"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCreateAndDeleteCollectionViaUi() throws Exception { + String name = "uicreated"; + openPage("~collections", By.id("collections")); + + // create through the Add Collection dialog + waitFor(By.cssSelector("#navigation button#add")).click(); + WebElement nameInput = waitFor(By.id("add_name")); + nameInput.clear(); + nameInput.sendKeys(name); + chosenSelect("add_config", COLLECTION); + WebElement numShards = waitFor(By.id("add_numShards")); + numShards.clear(); + numShards.sendKeys("1"); + WebElement replicationFactor = waitFor(By.id("add_replicationFactor")); + replicationFactor.clear(); + replicationFactor.sendKeys("1"); + waitFor(By.xpath("//button[@ng-click='addCollection()']")).click(); + + // the new collection shows up in the list, and the API confirms it + waitForPageContains(name); + assertCollectionExists(name, true); + + // delete it through the delete dialog, which requires typing the name to confirm + openPage("~collections/" + name, By.id("collections")); + waitFor(By.id("delete-collection")).click(); + WebElement confirmInput = waitFor(By.id("collectionDeleteConfirm")); + confirmInput.clear(); + confirmInput.sendKeys(name); + waitFor(By.xpath("//button[@ng-click='deleteCollection()']")).click(); + + assertCollectionExists(name, false); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCreateAndDeleteAliasViaUi() throws Exception { + String alias = "uialias"; + openPage("~collections", By.id("collections")); + + // the Create Alias button stays disabled until the collection list has loaded + waitForPageContains(COLLECTION); + waitFor(By.cssSelector("button#create-alias:not([disabled])")).click(); + WebElement aliasInput = waitFor(By.id("alias")); + aliasInput.clear(); + aliasInput.sendKeys(alias); + // the collections picker is a plain multi-select; click the option directly + waitFor(By.xpath("//select[@id='aliasCollections']/option[text()='" + COLLECTION + "']")) + .click(); + waitFor(By.xpath("//button[@ng-click='createAlias()']")).click(); + + waitUntil( + "alias " + alias + " should exist", + () -> listAliases().getOrDefault(alias, "").equals(COLLECTION)); + + // aliases are listed with an alias_ route prefix + openPage("~collections/alias_" + alias, By.id("collections")); + waitFor(By.id("delete-alias")).click(); + waitFor(By.xpath("//button[@ng-click='deleteAlias()']")).click(); + + waitUntil("alias " + alias + " should be gone", () -> !listAliases().containsKey(alias)); + assertNoSevereConsoleErrors(); + } + + @Test + public void testAddAndDeleteReplicaViaUi() throws Exception { + openPage("~collections/" + COLLECTION, By.id("collections")); + + // expand shard1 and open the add-replica form + waitFor(By.xpath("//div[@id='shard-data']//a[contains(., 'shard1')]")).click(); + waitFor(By.id("add-replica")).click(); + waitFor(By.xpath("//button[@ng-click='addReplica(shard)']")).click(); + + waitUntil("second replica should appear", () -> replicaCount() == 2); + + // delete the added replica: expand it and confirm removal within the same replica block + driver.navigate().refresh(); + waitFor(By.xpath("//div[@id='shard-data']//a[contains(., 'shard1')]")).click(); + waitFor(By.xpath("//a[@ng-click='toggleRemoveReplica(replica)']")); + List removeToggles = + driver.findElements(By.xpath("//a[@ng-click='toggleRemoveReplica(replica)']")); + assertEquals("Expected a remove toggle per replica", 2, removeToggles.size()); + WebElement toggle = removeToggles.get(1); + toggle.click(); + toggle + .findElement( + By.xpath( + "ancestor::ul[contains(@class,'replica')][1]" + + "//button[@ng-click='deleteReplica(replica)']")) + .click(); + + waitUntil("replica should be removed again", () -> replicaCount() == 1); + assertNoSevereConsoleErrors(); + } + + @Test + public void testReloadCollectionViaUi() throws Exception { + // reloading resets the core's start time; that proves the action end-to-end, + // unlike the UI success indicator which only flashes for a second + String coreName = coreNameOnNode0(COLLECTION); + Object startTimeBefore = coreStartTime(coreName); + + openPage("~collections/" + COLLECTION, By.id("collections")); + waitFor(By.id("reload")).click(); + waitUntil( + "core start time should change after reload", + () -> !startTimeBefore.equals(coreStartTime(coreName))); + assertNoSevereConsoleErrors(); + } + + private void assertCollectionExists(String name, boolean expectExists) throws Exception { + waitUntil( + "collection " + name + " should " + (expectExists ? "exist" : "not exist"), + () -> { + try { + return CollectionAdminRequest.listCollections(cluster.getSolrClient()).contains(name) + == expectExists; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private Map listAliases() { + try { + CollectionAdminResponse response = + new CollectionAdminRequest.ListAliases().process(cluster.getSolrClient()); + return response.getAliases(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Object coreStartTime(String coreName) { + try { + return adminApi("/admin/cores", params("core", coreName)) + ._get(List.of("status", coreName, "startTime"), null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private int replicaCount() { + try { + return cluster + .getSolrClient() + .getClusterState() + .getCollection(COLLECTION) + .getReplicas() + .size(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From fcfe63828bf8470077e05112f704718429e72798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 13/30] Admin UI tests: Query, Documents and Paramsets screen classes Feature-grouped classes: query execution with rows/fl parameters, the documents indexing form, and paramset create/delete via the UI. --- .../webapp/AdminUiDocumentsScreenTest.java | 77 ++++++++++++++++ .../webapp/AdminUiParamsetsScreenTest.java | 73 +++++++++++++++ .../solr/webapp/AdminUiQueryScreenTest.java | 91 +++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiDocumentsScreenTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDocumentsScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDocumentsScreenTest.java new file mode 100644 index 000000000000..f6db1224384d --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiDocumentsScreenTest.java @@ -0,0 +1,77 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.solr.client.solrj.request.SolrQuery; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Tests the Documents screen: the indexing form and submitting documents through it. */ +public class AdminUiDocumentsScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "docscoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + } + + @Test + public void testDocumentsScreenForm() { + openPage(COLLECTION + "/documents", By.id("documents")); + List types = + driver.findElements(By.cssSelector("#document-type option")).stream() + .map(WebElement::getText) + .collect(Collectors.toList()); + assertTrue("Doc type dropdown should offer JSON, got " + types, types.contains("JSON")); + assertTrue("Doc type dropdown should offer XML, got " + types, types.contains("XML")); + assertTrue("Doc type dropdown should offer CSV, got " + types, types.contains("CSV")); + waitFor(By.id("submit")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testIndexDocumentViaUi() throws Exception { + openPage(COLLECTION + "/documents", By.id("documents")); + WebElement docInput = waitFor(By.id("document")); + docInput.clear(); + docInput.sendKeys("{\"id\":\"ui-doc-1\",\"title_txt\":\"indexed from the admin ui\"}"); + waitFor(By.id("submit")).click(); + waitForTextContains(By.cssSelector("#documents #result"), "success"); + + // the document becomes searchable (the form defaults to commitWithin=1000) + waitUntil( + "document indexed via UI should be searchable", + () -> { + try { + return cluster + .getSolrClient(COLLECTION) + .query(new SolrQuery("id:ui-doc-1")) + .getResults() + .getNumFound() + == 1; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + assertNoSevereConsoleErrors(); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java new file mode 100644 index 000000000000..b6360d353670 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiParamsetsScreenTest.java @@ -0,0 +1,73 @@ +/* + * 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.webapp; + +import java.util.List; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Tests the Paramsets screen: form display and creating/deleting a paramset through the UI. */ +public class AdminUiParamsetsScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "paramscoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + } + + @Test + public void testParamsetsScreenRenders() { + openPage(COLLECTION + "/paramsets", By.id("paramsets")); + waitFor(By.cssSelector("#paramsets #form")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testCreateAndDeleteParamsetViaUi() throws Exception { + String paramset = "uiparams"; + openPage(COLLECTION + "/paramsets", By.id("paramsets")); + + WebElement content = waitFor(By.id("paramsetContent")); + content.clear(); + content.sendKeys("{\"set\":{\"" + paramset + "\":{\"rows\":\"7\",\"df\":\"title_txt\"}}}"); + waitFor(By.cssSelector("#paramsets #submit")).click(); + waitForTextContains(By.cssSelector("#paramsets #result"), "success"); + + waitUntil("paramset should exist with rows=7", () -> paramsetRows(paramset).equals("7")); + + // select the paramset and delete it + openPage(COLLECTION + "/paramsets?paramset=" + paramset, By.id("paramsets")); + waitFor(By.cssSelector("button#delete-paramset")).click(); + waitUntil("paramset should be gone", () -> paramsetRows(paramset).isEmpty()); + assertNoSevereConsoleErrors(); + } + + /** Returns the rows param of the paramset, or empty string when absent. */ + private String paramsetRows(String name) { + try { + Object rows = + adminApi("/" + COLLECTION + "/config/params/" + name, params()) + ._get(List.of("response", "params", name, "rows"), ""); + return rows == null ? "" : rows.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java new file mode 100644 index 000000000000..11796e0fd0df --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java @@ -0,0 +1,91 @@ +/* + * 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.webapp; + +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.common.SolrInputDocument; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Tests the Query screen: executing queries through the form and its parameter fields. */ +public class AdminUiQueryScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "querycoll"; + private static final int NUM_DOCS = 4; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + SolrClient client = cluster.getSolrClient(COLLECTION); + for (int i = 1; i <= NUM_DOCS; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", Integer.toString(i)); + doc.addField("title_txt", "Book number " + i); + client.add(doc); + } + client.commit(); + } + + @Test + public void testQueryScreenExecutesQueries() { + openPage(COLLECTION + "/query", By.id("query")); + + // default *:* query finds all documents + waitFor(By.cssSelector("#query button[type=submit]")).click(); + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); + + // a specific id query finds exactly one document + WebElement queryInput = waitFor(By.id("q")); + queryInput.clear(); + queryInput.sendKeys("id:1"); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":1"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testRowsAndFieldListParameters() { + openPage(COLLECTION + "/query", By.id("query")); + + WebElement rows = waitFor(By.id("rows")); + rows.clear(); + rows.sendKeys("2"); + WebElement fl = waitFor(By.id("fl")); + fl.clear(); + fl.sendKeys("id"); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + + String response = + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); + // only two docs are returned, and only their id field + assertEquals("Expected 2 returned docs: " + response, 2, countOccurrences(response, "\"id\":")); + assertFalse("fl=id should exclude other fields: " + response, response.contains("title_txt")); + assertNoSevereConsoleErrors(); + } + + private static int countOccurrences(String haystack, String needle) { + int count = 0; + int idx = 0; + while ((idx = haystack.indexOf(needle, idx)) >= 0) { + count++; + idx += needle.length(); + } + return count; + } +} From 16f70a2061a4ce237bdda76c9193301082977546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 14/30] Admin UI tests: Schema screen incl. field add/delete, and Schema Designer Schema browser display, field flags and term info, add/delete field via the dialogs. Nightly Schema Designer happy path: new schema from a sample document. --- .../webapp/AdminUiSchemaDesignerTest.java | 76 ++++++++++++ .../solr/webapp/AdminUiSchemaScreenTest.java | 114 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaScreenTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java new file mode 100644 index 000000000000..c90021a0fa00 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java @@ -0,0 +1,76 @@ +/* + * 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.webapp; + +import org.apache.lucene.tests.util.LuceneTestCase.Nightly; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Happy-path test of the Schema Designer screen: create a new schema, paste a sample document and + * let the designer analyze it. + * + *

Nightly: the designer chains many requests and is the most complex screen in the UI. + */ +@Nightly +public class AdminUiSchemaDesignerTest extends AdminUiTestBase { + + @Test + public void testDesignSchemaFromSampleDocument() throws Exception { + openPage("~schema-designer", By.id("designer")); + + // create a new schema via the dialog + waitFor(By.cssSelector("#designer #add")).click(); + WebElement schemaName = waitFor(By.id("add_schema")); + schemaName.clear(); + schemaName.sendKeys("uidesigned"); + waitFor(By.xpath("//button[@ng-click='addSchema()']")).click(); + + // paste a sample document and analyze it + WebElement sampleDocs = waitFor(By.cssSelector("#sample-docs textarea#document")); + sampleDocs.clear(); + sampleDocs.sendKeys("[{\"id\":\"1\",\"designer_title\":\"Hello Designer\"}]"); + waitFor(By.id("analyze")).click(); + + // the analyzed schema lists the field derived from the sample doc; the designer + // occasionally races itself persisting the schema ("version mismatch, retry") - in + // that case dismiss via the offered Reload Schema button and analyze again + waitUntil( + "analyzed schema should list the sample doc field", + () -> { + if (driver.getPageSource().contains("designer_title")) { + return true; + } + if (driver.getPageSource().contains("version mismatch")) { + driver.findElements(By.xpath("//button[contains(., 'Reload Schema')]")).stream() + .filter(WebElement::isDisplayed) + .findFirst() + .ifPresent(WebElement::click); + driver.findElements(By.id("analyze")).stream() + .filter(WebElement::isDisplayed) + .findFirst() + .ifPresent(WebElement::click); + } + return false; + }); + // the designer's own API calls (prep/analyze/luke against its temp core) error + // transiently while it persists and reloads the schema - it recovers via its retry + // dialog, so only unrelated console errors fail the test + assertNoSevereConsoleErrors("schema-designer/", "._designer_"); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaScreenTest.java new file mode 100644 index 000000000000..d89c8464e4be --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaScreenTest.java @@ -0,0 +1,114 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.util.NamedList; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Tests the Schema Browser screen: browsing fields and their flags, term info, and adding/deleting + * a field through the UI dialogs (the fixture uses a mutable managed schema). + */ +public class AdminUiSchemaScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "schemacoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + SolrClient client = cluster.getSolrClient(COLLECTION); + for (int i = 1; i <= 3; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "doc" + i); + client.add(doc); + } + client.commit(); + } + + @Test + public void testSchemaBrowserShowsFields() { + openPage(COLLECTION + "/schema", By.id("schema")); + // managed schema is editable, so the action buttons are shown + waitFor(By.id("addField")); + // known fields from the _default configset are browsable + waitForPageContains("_version_"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testFieldDetailShowsFlagsAndTermInfo() throws Exception { + // the id field of _default is indexed, stored and required per the schema API + NamedList response = adminApi("/" + COLLECTION + "/schema/fields/id", params()); + Map field = (Map) response.get("field"); + assertEquals(Boolean.TRUE, field.get("indexed")); + assertEquals(Boolean.TRUE, field.get("stored")); + + openPage(COLLECTION + "/schema?field=id", By.id("schema")); + // the detail header shows the selected field name + waitForTextContains(By.cssSelector("#schema span.name"), "id"); + // the flags matrix lists these properties for the field + waitForPageContains("Indexed"); + waitForPageContains("Stored"); + + // term info for the populated id field shows the indexed terms + waitFor(By.xpath("//button[@ng-click='toggleTerms()']")).click(); + waitForPageContains("doc1"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testAddAndDeleteFieldViaUi() throws Exception { + String fieldName = "ui_added_field"; + openPage(COLLECTION + "/schema", By.id("schema")); + + waitFor(By.id("addField")).click(); + WebElement nameInput = waitFor(By.id("add_name")); + nameInput.clear(); + nameInput.sendKeys(fieldName); + chosenSelect("add_type", "string"); + waitFor(By.xpath("//button[@ng-click='addField()']")).click(); + + waitUntil("field " + fieldName + " should exist in schema", () -> fieldExists(fieldName)); + + // delete it again from the field detail view + openPage(COLLECTION + "/schema?field=" + fieldName, By.id("schema")); + waitForTextContains(By.cssSelector("#schema span.name"), fieldName); + waitFor(By.xpath("//dd[contains(@class,'delete-field')]/button")).click(); + waitFor(By.xpath("//div[contains(@class,'delete')]//button[@ng-click='delete()']")).click(); + + waitUntil("field " + fieldName + " should be gone", () -> !fieldExists(fieldName)); + assertNoSevereConsoleErrors(); + } + + @SuppressWarnings("unchecked") + private boolean fieldExists(String fieldName) { + try { + NamedList response = adminApi("/" + COLLECTION + "/schema/fields", params()); + List> fields = (List>) response.get("fields"); + return fields.stream().anyMatch(f -> fieldName.equals(f.get("name"))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 6e3fab72333eb9cfd6201a2c730a2f4cd4f11fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 15/30] Admin UI tests: Logging, Core Admin, Stream and Replication screens Logging level editor and events viewer, core reload, streaming expression execution, and replication screen rendering in cloud mode. --- .../webapp/AdminUiCoreAdminScreenTest.java | 56 +++++++++ .../solr/webapp/AdminUiLoggingScreenTest.java | 113 ++++++++++++++++++ .../webapp/AdminUiReplicationScreenTest.java | 46 +++++++ .../solr/webapp/AdminUiStreamScreenTest.java | 54 +++++++++ 4 files changed, 269 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminScreenTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationScreenTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminScreenTest.java new file mode 100644 index 000000000000..71ec933fbb5c --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminScreenTest.java @@ -0,0 +1,56 @@ +/* + * 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.webapp; + +import java.util.Map; +import org.apache.solr.common.util.NamedList; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; + +/** Tests the Core Admin screen: core listing and the reload action. */ +public class AdminUiCoreAdminScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "corescoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + } + + @Test + public void testCoreAdminShowsCore() throws Exception { + NamedList response = adminApi("/admin/cores", params()); + Map status = (Map) response.get("status"); + assertFalse("Node should host at least one core", status.isEmpty()); + String coreName = status.keySet().iterator().next().toString(); + + openPage("~cores", By.id("cores")); + waitForPageContains(coreName); + assertNoSevereConsoleErrors(); + } + + @Test + public void testReloadCoreViaUi() { + String coreName = coreNameOnNode0(COLLECTION); + openPage("~cores/" + coreName, By.id("cores")); + waitFor(By.cssSelector("#cores #reload")).click(); + // the button is marked with the success class when the reload succeeded + waitFor(By.cssSelector("#cores #reload.success")); + assertNoSevereConsoleErrors(); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java new file mode 100644 index 000000000000..c96404874c74 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java @@ -0,0 +1,113 @@ +/* + * 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.webapp; + +import java.util.List; +import java.util.Map; +import org.apache.solr.common.util.NamedList; +import org.junit.Assume; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.slf4j.LoggerFactory; + +/** Tests the Logging screens: the recent-events viewer and the log level editor. */ +public class AdminUiLoggingScreenTest extends AdminUiTestBase { + + @Test + public void testLoggingLevelTree() { + openPage("~logging/level", By.id("loggingtree")); + waitForPageContains("org.apache.solr"); + waitFor(By.cssSelector("#loggingtree .jstree-anchor")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testEventsViewerShowsWarnings() throws Exception { + // the cluster nodes run in this JVM, so the log watcher observes our own log events + String probeMessage = "Admin UI logging viewer probe event"; + LoggerFactory.getLogger(AdminUiLoggingScreenTest.class).warn(probeMessage); + + // all test clusters in this JVM register a log-watcher appender under the same name + // in the shared log4j config, so this cluster's watcher may be blind when other UI + // test classes ran first; only assert the UI when the backing API sees the event + boolean watcherSawProbe = false; + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + while (System.nanoTime() < deadlineNanos && !watcherSawProbe) { + watcherSawProbe = + adminApi("/admin/info/logging", params("since", "0")).toString().contains(probeMessage); + Thread.sleep(250); + } + Assume.assumeTrue( + "This node's log watcher does not receive events (shared-JVM log4j state); skipping", + watcherSawProbe); + + openPage("~logging", By.id("viewer")); + waitUntil( + "probe event should appear in the viewer", + () -> { + driver.navigate().refresh(); + waitFor(By.id("viewer")); + return driver.getPageSource().contains(probeMessage); + }); + assertNoSevereConsoleErrors(); + } + + @Test + public void testChangeLogLevelViaUi() throws Exception { + String logger = "org.apache.solr.core"; + openPage("~logging/level", By.id("loggingtree")); + + WebElement anchor = + waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")); + anchor.click(); + waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='WARN']")).click(); + assertLoggerLevel(logger, "WARN"); + + // revert to unset; the logger then reports the inherited level with set=false + waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")).click(); + waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='UNSET']")).click(); + assertLoggerLevel(logger, null); + assertNoSevereConsoleErrors(); + } + + /** + * Asserts the level a logger was explicitly set to, or with {@code expectedLevel} null, that the + * logger has no explicit level (it then reports the inherited effective level with set=false). + */ + @SuppressWarnings("unchecked") + private void assertLoggerLevel(String logger, String expectedLevel) throws Exception { + waitUntil( + "logger " + logger + " has level " + (expectedLevel == null ? "(unset)" : expectedLevel), + () -> { + try { + NamedList response = adminApi("/admin/info/logging", params()); + for (Map entry : (List>) response.get("loggers")) { + if (logger.equals(entry.get("name"))) { + return expectedLevel == null + ? Boolean.FALSE.equals(entry.get("set")) + : expectedLevel.equals(entry.get("level")) + && Boolean.TRUE.equals(entry.get("set")); + } + } + return false; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationScreenTest.java new file mode 100644 index 000000000000..568d7c8b814a --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationScreenTest.java @@ -0,0 +1,46 @@ +/* + * 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.webapp; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Tests the Replication screen in cloud mode. The screen targets standalone leader/follower + * replication; in cloud mode it should still render the core's index version information. + * Standalone-mode coverage (replicate-now, enable/disable polling) needs a non-cloud harness and is + * not covered here. + */ +public class AdminUiReplicationScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "replcoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + } + + @Test + public void testReplicationScreenRenders() { + String coreName = coreNameOnNode0(COLLECTION); + openPage(coreName + "/replication", By.id("replication")); + // the details block shows the index version table + waitForPageContains("Version"); + assertNoSevereConsoleErrors(); + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java new file mode 100644 index 000000000000..8e4125596ef2 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java @@ -0,0 +1,54 @@ +/* + * 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.webapp; + +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.common.SolrInputDocument; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** Tests the Stream screen: executing a streaming expression through the form. */ +public class AdminUiStreamScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "streamcoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + SolrClient client = cluster.getSolrClient(COLLECTION); + for (int i = 1; i <= 3; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "stream-doc-" + i); + client.add(doc); + } + client.commit(); + } + + @Test + public void testStreamingExpressionViaUi() { + openPage(COLLECTION + "/stream", By.id("stream")); + WebElement expr = waitFor(By.id("expr")); + expr.clear(); + expr.sendKeys("search(" + COLLECTION + ",q=\"*:*\",fl=\"id\",sort=\"id asc\")"); + waitFor(By.cssSelector("#stream button[type=submit]")).click(); + String response = waitForTextContains(By.cssSelector("#stream #result"), "stream-doc-1"); + assertTrue("All docs should stream: " + response, response.contains("stream-doc-3")); + assertNoSevereConsoleErrors(); + } +} From 6fbacbc326a6025f3951d2fce4aec5cc55495ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 16/30] Admin UI tests: finish feature-based regrouping; cloud graph/zkstatus depth Removes AdminUiWriteActionsTest (its tests moved to the feature classes), trims the node/collection display classes accordingly, and deepens the cloud coverage: graph SVG replica circles and ZK status ensemble info. --- .../webapp/AdminUiCollectionScreensTest.java | 104 ++-------- .../solr/webapp/AdminUiNodeScreensTest.java | 47 ++--- .../solr/webapp/AdminUiWriteActionsTest.java | 177 ------------------ 3 files changed, 37 insertions(+), 291 deletions(-) delete mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java index b2a8bd5e7940..85b813ea9d52 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCollectionScreensTest.java @@ -16,19 +16,17 @@ */ package org.apache.solr.webapp; -import java.util.List; -import java.util.stream.Collectors; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.common.SolrInputDocument; -import org.apache.solr.util.ExternalPaths; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; /** - * Verifies the per-collection Admin UI screens against a fixture collection with indexed documents. + * Verifies the per-collection analysis/files/segments/plugins/overview screens against a fixture + * collection with indexed documents. Screens with their own write actions have dedicated test + * classes (query, documents, schema, paramsets). */ public class AdminUiCollectionScreensTest extends AdminUiTestBase { @@ -37,14 +35,7 @@ public class AdminUiCollectionScreensTest extends AdminUiTestBase { @BeforeClass public static void setupCollection() throws Exception { - cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); - // pin the replica to the node the browser talks to, so core-level screens - // (plugins, segments) find it locally - CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 1) - .setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()) - .process(cluster.getSolrClient()); - cluster.waitForActiveCollection(COLLECTION, 1, 1); - + createFixtureCollection(COLLECTION, 1, 1); SolrClient client = cluster.getSolrClient(COLLECTION); for (int i = 1; i <= NUM_DOCS; i++) { SolrInputDocument doc = new SolrInputDocument(); @@ -55,23 +46,6 @@ public static void setupCollection() throws Exception { client.commit(); } - @Test - public void testQueryScreenExecutesQueries() { - openPage(COLLECTION + "/query", By.id("query")); - - // default *:* query finds all documents - waitFor(By.cssSelector("#query button[type=submit]")).click(); - waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); - - // a specific id query finds exactly one document - WebElement queryInput = waitFor(By.id("q")); - queryInput.clear(); - queryInput.sendKeys("id:1"); - waitFor(By.cssSelector("#query button[type=submit]")).click(); - waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":1"); - assertNoSevereConsoleErrors(); - } - @Test public void testAnalysisScreenAnalyzesText() { openPage(COLLECTION + "/analysis", By.id("analysis-holder")); @@ -86,16 +60,6 @@ public void testAnalysisScreenAnalyzesText() { assertNoSevereConsoleErrors(); } - @Test - public void testSchemaScreenShowsFields() { - openPage(COLLECTION + "/schema", By.id("schema")); - // managed schema is editable, so the action buttons are shown - waitFor(By.id("addField")); - // known fields from the _default configset are browsable - waitForPageContains("_version_"); - assertNoSevereConsoleErrors(); - } - @Test public void testFilesScreenShowsConfig() { openPage(COLLECTION + "/files", By.id("files")); @@ -108,50 +72,22 @@ public void testFilesScreenShowsConfig() { @Test public void testSegmentsScreenShowsSegments() throws Exception { - String coreName = coreNameOnNode0(); + String coreName = coreNameOnNode0(COLLECTION); openPage(coreName + "/segments", By.id("segments")); - long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); - List segments = List.of(); - while (System.nanoTime() < deadlineNanos) { - segments = driver.findElements(By.cssSelector("#segments #response li")); - if (!segments.isEmpty()) break; - Thread.sleep(200); - } - assertFalse("Expected at least one segment after committing docs", segments.isEmpty()); + waitUntil( + "at least one segment should render after committing docs", + () -> !driver.findElements(By.cssSelector("#segments #response li")).isEmpty()); assertNoSevereConsoleErrors(); } @Test - public void testPluginsScreenShowsStats() throws Exception { - String coreName = coreNameOnNode0(); + public void testPluginsScreenShowsStats() { + String coreName = coreNameOnNode0(COLLECTION); openPage(coreName + "/plugins", By.id("plugins")); waitForPageContains("searcher"); assertNoSevereConsoleErrors(); } - @Test - public void testDocumentsScreenForm() { - openPage(COLLECTION + "/documents", By.id("documents")); - List types = - driver.findElements(By.cssSelector("#document-type option")).stream() - .map(WebElement::getText) - .collect(Collectors.toList()); - assertTrue("Doc type dropdown should offer JSON, got " + types, types.contains("JSON")); - assertTrue("Doc type dropdown should offer XML, got " + types, types.contains("XML")); - assertTrue("Doc type dropdown should offer CSV, got " + types, types.contains("CSV")); - waitFor(By.id("submit")); - assertNoSevereConsoleErrors(); - } - - @Test - public void testParamsetsScreenRenders() { - openPage(COLLECTION + "/paramsets", By.id("paramsets")); - waitFor(By.cssSelector("#paramsets #form")); - // the shared menu code intermittently throws a benign TypeError while the - // per-collection menu resolves; the screen itself renders fine - assertNoSevereConsoleErrors("Cannot read properties of null (reading 'name')"); - } - @Test public void testCollectionOverviewShowsShard() { openPage(COLLECTION + "/collection-overview", By.id("dashboard")); @@ -159,17 +95,13 @@ public void testCollectionOverviewShowsShard() { assertNoSevereConsoleErrors(); } - /** Returns the fixture collection's core name on node 0, the node the browser talks to. */ - private static String coreNameOnNode0() { - for (String name : cluster.getJettySolrRunner(0).getCoreContainer().getAllCoreNames()) { - if (name.startsWith(COLLECTION + "_")) { - return name; - } - } - throw new AssertionError("No core found on node 0 for collection " + COLLECTION); - } - - private static String abbreviate(String s) { - return s.length() > 300 ? s.substring(0, 300) + "..." : s; + @Test + public void testCoreOverviewShowsStats() { + String coreName = coreNameOnNode0(COLLECTION); + openPage(coreName + "/core-overview", By.id("dashboard")); + waitForPageContains("Num Docs"); + waitForPageContains(Integer.toString(NUM_DOCS)); + // the ping widget answers 503 when the configset has no healthcheck file + assertNoSevereConsoleErrors("/admin/ping"); } } diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java index ab10ee6808e0..681815f6b83c 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiNodeScreensTest.java @@ -18,25 +18,23 @@ import java.util.List; import java.util.Map; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.common.util.NamedList; -import org.apache.solr.util.ExternalPaths; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; -/** Verifies the data displayed on the node-level Admin UI screens against the backing APIs. */ +/** + * Verifies the data displayed on the node-level Admin UI screens (java properties, thread dump, + * cloud views, security, login) against the backing APIs. + */ public class AdminUiNodeScreensTest extends AdminUiTestBase { private static final String COLLECTION = "nodescoll"; @BeforeClass public static void setupCollection() throws Exception { - cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); - CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 2) - .process(cluster.getSolrClient()); - cluster.waitForActiveCollection(COLLECTION, 1, 2); + createFixtureCollection(COLLECTION, 1, 2); } @Test @@ -51,9 +49,9 @@ public void testJavaPropertiesMatchApi() throws Exception { // spaces (​) into names and values for line wrapping, so strip them String value = null; for (WebElement row : driver.findElements(By.cssSelector("#java-properties li"))) { - String name = row.findElement(By.cssSelector("dt")).getText().replace("\u200B", ""); + String name = row.findElement(By.cssSelector("dt")).getText().replace("​", ""); if (name.equals("java.version")) { - value = row.findElement(By.cssSelector("dd")).getText().replace("\u200B", ""); + value = row.findElement(By.cssSelector("dd")).getText().replace("​", ""); } } assertEquals(expectedJavaVersion, value); @@ -70,15 +68,6 @@ public void testThreadDumpShowsThreads() { assertNoSevereConsoleErrors(); } - @Test - public void testLoggingLevelTree() { - openPage("~logging/level", By.id("loggingtree")); - waitForPageContains("org.apache.solr"); - // the level legend/menu offers the standard levels - waitFor(By.cssSelector("#loggingtree .jstree-anchor")); - assertNoSevereConsoleErrors(); - } - @Test public void testCloudNodesListsAllNodes() { openPage("~cloud?view=nodes", By.id("nodes-table")); @@ -101,22 +90,24 @@ public void testCloudTreeShowsZkNodes() { } @Test - public void testCollectionsScreenShowsCollectionDetail() { - openPage("~collections/" + COLLECTION, By.id("collections")); + public void testCloudGraphShowsReplicas() throws Exception { + openPage("~cloud?view=graph", By.id("graph-content")); + // the d3 tree renders one circle per zk/collection/shard/replica node; the fixture + // collection has two replicas, so expect at least: root + collection + shard + 2 replicas + waitUntil( + "graph should render circles", + () -> driver.findElements(By.cssSelector("#graph-content svg circle")).size() >= 5); waitForPageContains(COLLECTION); waitForPageContains("shard1"); assertNoSevereConsoleErrors(); } @Test - public void testCoreAdminShowsCore() throws Exception { - NamedList response = adminApi("/admin/cores", params()); - Map status = (Map) response.get("status"); - assertFalse("Node should host at least one core", status.isEmpty()); - String coreName = status.keySet().iterator().next().toString(); - - openPage("~cores", By.id("cores")); - waitForPageContains(coreName); + public void testCloudZkStatusShowsEnsemble() { + openPage("~cloud?view=zkstatus", By.id("zk-status-content")); + // the embedded test ensemble is a single standalone zookeeper reported green + waitForTextContains(By.cssSelector(".zookeeper-status"), "green"); + waitForPageContains("Ensemble size: 1"); assertNoSevereConsoleErrors(); } diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java deleted file mode 100644 index 44b23b7dda73..000000000000 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiWriteActionsTest.java +++ /dev/null @@ -1,177 +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.webapp; - -import java.util.List; -import java.util.Map; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.client.solrj.request.SolrQuery; -import org.apache.solr.common.util.NamedList; -import org.apache.solr.util.ExternalPaths; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; - -/** Exercises write actions performed through the Admin UI, verifying the effect via the APIs. */ -public class AdminUiWriteActionsTest extends AdminUiTestBase { - - private static final String CONFIG = "writeconf"; - private static final String COLLECTION = "writecoll"; - - @BeforeClass - public static void setupFixture() throws Exception { - cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, CONFIG); - CollectionAdminRequest.createCollection(COLLECTION, CONFIG, 1, 1) - .setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()) - .process(cluster.getSolrClient()); - cluster.waitForActiveCollection(COLLECTION, 1, 1); - } - - @Test - public void testCreateAndDeleteCollectionViaUi() throws Exception { - String name = "uicreated"; - openPage("~collections", By.id("collections")); - - // create through the Add Collection dialog - waitFor(By.cssSelector("#navigation button#add")).click(); - WebElement nameInput = waitFor(By.id("add_name")); - nameInput.clear(); - nameInput.sendKeys(name); - chosenSelect("add_config", CONFIG); - WebElement numShards = waitFor(By.id("add_numShards")); - numShards.clear(); - numShards.sendKeys("1"); - WebElement replicationFactor = waitFor(By.id("add_replicationFactor")); - replicationFactor.clear(); - replicationFactor.sendKeys("1"); - waitFor(By.xpath("//button[@ng-click='addCollection()']")).click(); - - // the new collection shows up in the list, and the API confirms it - waitForPageContains(name); - assertCollectionExists(name, true); - - // delete it through the delete dialog, which requires typing the name to confirm - openPage("~collections/" + name, By.id("collections")); - waitFor(By.id("delete-collection")).click(); - WebElement confirmInput = waitFor(By.id("collectionDeleteConfirm")); - confirmInput.clear(); - confirmInput.sendKeys(name); - waitFor(By.xpath("//button[@ng-click='deleteCollection()']")).click(); - - assertCollectionExists(name, false); - assertNoSevereConsoleErrors(); - } - - @Test - public void testIndexDocumentViaUi() throws Exception { - openPage(COLLECTION + "/documents", By.id("documents")); - WebElement docInput = waitFor(By.id("document")); - docInput.clear(); - docInput.sendKeys("{\"id\":\"ui-doc-1\",\"title_txt\":\"indexed from the admin ui\"}"); - waitFor(By.id("submit")).click(); - waitForTextContains(By.cssSelector("#documents #result"), "success"); - - // the document becomes searchable (the form defaults to commitWithin=1000) - long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); - long numFound = 0; - while (System.nanoTime() < deadlineNanos) { - numFound = - cluster - .getSolrClient(COLLECTION) - .query(new SolrQuery("id:ui-doc-1")) - .getResults() - .getNumFound(); - if (numFound > 0) break; - Thread.sleep(250); - } - assertEquals("Document indexed via UI should be searchable", 1, numFound); - assertNoSevereConsoleErrors(); - } - - @Test - public void testChangeLogLevelViaUi() throws Exception { - String logger = "org.apache.solr.core"; - openPage("~logging/level", By.id("loggingtree")); - - WebElement anchor = - waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")); - anchor.click(); - waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='WARN']")).click(); - assertLoggerLevel(logger, "WARN"); - - // revert to unset; the logger then reports the inherited level with set=false - waitFor(By.cssSelector("#loggingtree a.jstree-anchor[title='" + logger + "']")).click(); - waitFor(By.xpath("//li[a/@title='" + logger + "']//a[normalize-space()='UNSET']")).click(); - assertLoggerLevel(logger, null); - assertNoSevereConsoleErrors(); - } - - private void assertCollectionExists(String name, boolean expectExists) throws Exception { - long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); - boolean exists = !expectExists; - while (System.nanoTime() < deadlineNanos) { - List collections = CollectionAdminRequest.listCollections(cluster.getSolrClient()); - exists = collections.contains(name); - if (exists == expectExists) return; - Thread.sleep(250); - } - fail( - "Collection " - + name - + " should " - + (expectExists ? "" : "not ") - + "exist, but does" - + (exists ? "" : " not")); - } - - /** - * Asserts the level a logger was explicitly set to, or with {@code expectedLevel} null, that the - * logger has no explicit level (it then reports the inherited effective level with set=false). - */ - @SuppressWarnings("unchecked") - private void assertLoggerLevel(String logger, String expectedLevel) throws Exception { - long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); - Object actualLevel = "(logger not found)"; - Object actualSet = null; - while (System.nanoTime() < deadlineNanos) { - NamedList response = adminApi("/admin/info/logging", params()); - for (Map entry : (List>) response.get("loggers")) { - if (logger.equals(entry.get("name"))) { - actualLevel = entry.get("level"); - actualSet = entry.get("set"); - } - } - boolean matches = - expectedLevel == null - ? Boolean.FALSE.equals(actualSet) - : expectedLevel.equals(actualLevel) && Boolean.TRUE.equals(actualSet); - if (matches) return; - Thread.sleep(250); - } - fail( - "Logger " - + logger - + " expected level " - + (expectedLevel == null ? "(unset)" : expectedLevel) - + " but was " - + actualLevel - + " (set=" - + actualSet - + ")"); - } -} From 26d502236cec769e593a8ee66d9758a7cc4a751e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 17/30] Admin UI tests: BasicAuth login and Security screen (nightly) Cluster bootstrapped with BasicAuth security.json; the login form flow, security screen display and adding a user via the dialog, verified via the authentication API. --- .../solr/webapp/AdminUiSecurityAuthTest.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java new file mode 100644 index 000000000000..14347e5580e6 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java @@ -0,0 +1,124 @@ +/* + * 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.webapp; + +import java.util.Map; +import org.apache.lucene.tests.util.LuceneTestCase.Nightly; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.common.util.NamedList; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebElement; + +/** + * Tests the Admin UI with BasicAuth enabled: the login screen flow and the Security screen, + * including adding a user through the UI dialog. + * + *

Nightly: the login/session interplay between the browser and the auth filter is the most + * timing-sensitive part of the UI test suite. + */ +@Nightly +public class AdminUiSecurityAuthTest extends AdminUiTestBase { + + private static final String USER = "solr"; + private static final String PASS = "SolrRocks"; + + static { + // consumed by AdminUiTestBase when starting the cluster + securityJson = + "{\n" + + " \"authentication\": {\n" + + " \"blockUnknown\": true,\n" + + " \"class\": \"solr.BasicAuthPlugin\",\n" + + " \"credentials\": {\"solr\": \"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0=" + + " Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c=\"}\n" + + " },\n" + + " \"authorization\": {\n" + + " \"class\": \"solr.RuleBasedAuthorizationPlugin\",\n" + + " \"permissions\": [{\"name\": \"security-edit\", \"role\": \"admin\"},\n" + + " {\"name\": \"all\", \"role\": \"admin\"}],\n" + + " \"user-role\": {\"solr\": \"admin\"}\n" + + " }\n" + + "}"; + } + + @Test + public void testLoginAndSecurityScreen() throws Exception { + // an unauthenticated visit is redirected to the login screen + driver.get(baseUrl + "/index.html#/"); + waitFor(By.id("login")); + WebElement username = waitFor(By.id("username")); + username.clear(); + username.sendKeys(USER); + WebElement password = waitFor(By.id("password")); + password.clear(); + password.sendKeys(PASS); + waitFor(By.xpath("//div[@id='login']//button[@type='submit']")).click(); + + // after login the dashboard loads and shows the authenticated security info + waitFor(By.id("index")); + waitForPageContains("BasicAuthPlugin"); + + // the security screen shows the configured plugins and users + openPage("~security", By.id("securityPanel")); + waitForPageContains("BasicAuthPlugin"); + waitForPageContains("RuleBasedAuthorizationPlugin"); + waitForPageContains(USER); + + // add a user through the dialog. The dialog is driven via the controller scope: + // native clicks/keystrokes into this absolutely-positioned dialog proved unreliable + // in headless mode, and per-keystroke entry is already covered by the login form. + String newUser = "uitestuser"; + String newUserPass = "Uitest!Pass99"; + waitFor(By.id("add-user")); + ((JavascriptExecutor) driver) + .executeScript( + "var scope = angular.element(document.getElementById('add-user')).scope();" + + " scope.showAddUserDialog(); scope.$apply();"); + waitFor(By.id("add_user")); + ((JavascriptExecutor) driver) + .executeScript( + "var scope = angular.element(document.getElementById('add_user')).scope();" + + " scope.upsertUser = {username: arguments[0], password: arguments[1]," + + " password2: arguments[1]};" + + " scope.doUpsertUser(); scope.$apply();", + newUser, + newUserPass); + + waitUntil("user " + newUser + " should exist", () -> userExists(newUser)); + // the users list refreshes to include the new user + waitForPageContains(newUser); + } + + /** Checks via the authentication API (with credentials) whether the user exists. */ + private boolean userExists(String user) { + try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { + GenericSolrRequest req = + new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/authentication", params()); + req.setBasicAuthCredentials(USER, PASS); + NamedList response = client.request(req); + Map authentication = (Map) response.get("authentication"); + Map credentials = (Map) authentication.get("credentials"); + return credentials.containsKey(user); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 621ca01a3f97109589af22d59101d877d1d45e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 00:55:54 +0200 Subject: [PATCH 18/30] Update Admin UI test plan doc: per-screen coverage and skipped items --- dev-docs/admin-ui-tests.md | 185 ++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 74 deletions(-) diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md index 8a68d5c91daf..3a27d58281e3 100644 --- a/dev-docs/admin-ui-tests.md +++ b/dev-docs/admin-ui-tests.md @@ -31,94 +31,131 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI The matching chromedriver is provisioned (and cached) by Selenium Manager. - Display assertions compare UI text against live JSON from the same node's admin APIs — never hardcoded values. -- Run with: `./gradlew :solr:webapp:test` - -## Phase 1 — Smoke/navigation (`AdminUiSmokeTest`) - -Navigate every route, wait for a screen-specific anchor element, assert no -severe browser console errors. - -- [x] Node-level routes: `/`, `~logging`, `~logging/level`, `~cloud?view=nodes`, - `~cloud?view=tree`, `~cloud?view=zkstatus`, `~cloud?view=graph`, `~cores`, - `~collections`, `~schema-designer`, `~security`, `~java-properties`, - `~threads`, `login` -- [x] Per-collection routes (fixture collection): `collection-overview`, - `analysis`, `documents`, `files`, `query`, `stream`, `paramsets`, - `schema`; per-core routes: `core-overview`, `plugins`, `segments` - -Flaky-risk flags: `~cloud?view=graph` (d3 svg async), `~cloud?view=zkstatus` -(ZK admin-command availability in the embedded ensemble), `~schema-designer` -(many chained requests), `sqlquery` (needs sql module — excluded). - -## Phase 2 — Node-level screens, display depth - -- [x] Dashboard (`AdminUiDashboardTest`): versions, JVM info, memory bars, - security warning vs `/admin/info/system` +- Tests are grouped per screen/feature, so each screen's display and write + tests live in the same class. +- On failure, a screenshot, the page source and the browser console log are + saved into the test temp dir. +- Run with: `./gradlew :solr:webapp:test` (add `-Ptests.nightly=true` for the + security and schema-designer classes) + +## Coverage by screen + +### Smoke navigation — `AdminUiSmokeTest` +- [x] Every node-level route (`/`, `~logging`, `~logging/level`, + `~cloud?view=nodes|tree|zkstatus|graph`, `~cores`, `~collections`, + `~schema-designer`, `~security`, `~java-properties`, `~threads`, `login`), + every per-collection route and the per-core routes render their main + content element without severe browser console errors. + +### Dashboard — `AdminUiDashboardTest` +- [x] Versions, JVM info and memory bars vs `/admin/info/system`; security + warning when security is disabled. + +### Node-level screens — `AdminUiNodeScreensTest` - [x] Java Properties: `java.version` value matches `/admin/info/properties` - (`AdminUiNodeScreensTest`) - [x] Thread Dump: thread list non-empty, Jetty worker thread shown -- [x] Logging: logger tree renders with `org.apache.solr` row - [x] Cloud > Nodes: one row per live node, ports match the cluster - [x] Cloud > Tree: `live_nodes` and `collections` znodes shown -- [x] Cloud > ZK Status / Graph: render without console errors (smoke only) -- [x] Collections: collection listed; detail shows shard info -- [x] Core Admin: hosted core name shown, matching `/admin/cores` -- [x] Security: "security is not enabled" warning panel (no auth configured) -- [x] Login: authentication info page shown when no authenticationPlugin -- [ ] Deeper assertions: cloud graph replica leaves, ZK status ensemble - details, logging events viewer content - -## Phase 3 — Per-collection screens (fixture: collection with pre-indexed docs) - -Covered by `AdminUiCollectionScreensTest`: - -- [x] Collection Overview: shard info shown -- [x] Query: `*:*` finds all fixture docs, `id:` query finds exactly one +- [x] Cloud > Graph: d3 SVG renders circles for collection/shard/replicas +- [x] Cloud > ZK Status: status green, ensemble size shown +- [x] Security screen: "not enabled" warning without auth +- [x] Login screen: authentication info page without auth + +### Collections screen — `AdminUiCollectionsScreenTest` +- [x] Collection detail display (shards) +- [x] Create + delete collection via the dialogs (verified via API) +- [x] Create + delete alias via the dialogs (verified via LISTALIASES) +- [x] Add + delete replica via the shard detail (verified via cluster state) +- [x] Reload collection (verified via core start time reset) + +### Query screen — `AdminUiQueryScreenTest` +- [x] `*:*` and `id:` queries via the form, `numFound` in the response +- [x] `rows` and `fl` parameters affect the returned documents +- [ ] Paramsets dropdown, dismax/edismax toggles, raw query parameters + +### Documents screen — `AdminUiDocumentsScreenTest` +- [x] Form renders with JSON/XML/CSV document types +- [x] Index a JSON document via the form; becomes searchable + +### Schema screen — `AdminUiSchemaScreenTest` +- [x] Field list browsable, editable-schema action buttons shown +- [x] Field detail shows flags matching `/schema/fields`; term info loads +- [x] Add + delete a field via the dialogs (verified via `/schema/fields`) + +### Paramsets screen — `AdminUiParamsetsScreenTest` +- [x] Form renders +- [x] Create + delete a paramset via the form (verified via `/config/params`) + +### Logging screens — `AdminUiLoggingScreenTest` +- [x] Logger level tree renders +- [x] Set + unset a logger level via the tree (verified via API) +- [x] Events viewer shows a WARN event logged in the server JVM (skips itself + when the node's log watcher is blind due to shared-JVM log4j state, see + Known limitations) + +### Core Admin screen — `AdminUiCoreAdminScreenTest` +- [x] Hosted core listed, matching `/admin/cores` +- [x] Reload core via the button (success indicator) +- [ ] Add/rename/swap/unload core — cloud-mode core admin operations conflict + with the Overseer; needs a standalone-mode harness + +### Per-collection display screens — `AdminUiCollectionScreensTest` - [x] Analysis: `text_general` tokenizes and lowercases entered text -- [x] Documents (display): doc-type dropdown offers JSON/XML/CSV, submit present -- [x] Schema Browser: editable-schema action buttons, `_version_` field listed - [x] Files: tree lists `solrconfig.xml`, file content renders -- [x] Plugins/Stats: searcher stats present (needs `metricsEnabled=true`) - [x] Segments: at least one segment rendered after commit -- [x] Paramsets (display): form renders -- [ ] Query: paramsets dropdown, dismax/edismax toggles, raw query params -- [ ] Schema Browser: per-field flags vs `/schema` API, term info loading -- [ ] Stream: simple streaming expression executes and renders result -- [ ] Replication in cloud mode; standalone-mode coverage deferred - -## Phase 4 — Write actions through the UI - -Covered by `AdminUiWriteActionsTest`: - -- [x] Collections: create collection via dialog → verify via API → delete via - UI with typed confirmation → gone -- [x] Documents: submit JSON doc via form → success response → searchable -- [x] Logging: set logger to WARN via level editor → verify via API → revert - to unset -- [ ] Collections: create/delete alias, add/delete replica, reload -- [ ] Schema Browser: add field → verify via `/schema/fields` → delete field -- [ ] Core Admin: RELOAD core via UI (rename/swap/unload deferred to a - standalone-mode class) -- [ ] Paramsets: create paramset via UI → verify via `/config/params` -- [ ] Security with BasicAuth (`@Nightly`): bootstrap `security.json`, login via - form, add user/role/permission, verify via security APIs (high flake risk) -- [ ] Schema Designer happy path (`@Nightly`, high flake risk) - -Policy: phases 1–3 run in the default test run; heavyweight phase-4 classes -(security, schema designer) are `@Nightly`. +- [x] Plugins/Stats: searcher stats present (needs `metricsEnabled=true`) +- [x] Collection overview: shard info; Core overview: numDocs + +### Stream screen — `AdminUiStreamScreenTest` +- [x] A `search(...)` streaming expression executes and renders all docs + +### Replication screen — `AdminUiReplicationScreenTest` +- [x] Renders index version info in cloud mode +- [ ] Standalone leader/follower actions (replicate now, disable polling) — + needs a standalone-mode harness + +### Security with BasicAuth — `AdminUiSecurityAuthTest` (`@Nightly`) +- [x] Unauthenticated visit redirects to login; login form authenticates +- [x] Security screen shows authn/authz plugins, users, roles, permissions +- [x] Add a user through the dialog (verified via `/admin/authentication`) +- [ ] Add role / add permission dialogs + +### Schema Designer — `AdminUiSchemaDesignerTest` (`@Nightly`) +- [x] Create a new schema, paste a sample doc, analyze; derived field shown + +## Deliberately skipped (effort vs value) + +- **SQL screen**: needs the `sql` module (Calcite and friends) on the webapp + test classpath, dragging in many jars and license files for one screen. +- **JWT/OAuth login flows**: require an external identity provider or heavy + mocking; BasicAuth covers the UI's login/session mechanics. +- **Keystroke-level entry in the security dialogs**: native clicks/keystrokes + into the absolutely-positioned dialogs proved unreliable in headless Chrome; + the dialogs are driven via the Angular controller scope instead. Keyboard + entry is covered by the login form and the other screens' forms. +- **Standalone (non-cloud) mode screens**: replication actions and core admin + rename/swap/unload need a standalone harness (`JettySolrRunner` without ZK); + the cloud harness covers everything else. ## Known limitations - The generated js-client bundle (`libs/solr/index.js`) only exists inside the built WAR, not in the source tree tests serve from. `AdminUiTestBase` serves a minimal stub defining the `solrApi` global (only `reloadCollection` is used - by the AngularJS UI) so the Collections screen works; a future improvement - could serve the real bundle when it has been built. -- The shared menu code intermittently logs a benign - `TypeError: Cannot read properties of null (reading 'name')` while the - per-collection menu resolves; allowed in the paramsets test. + by the AngularJS UI); a future improvement could serve the real bundle when + it has been built. +- Every test cluster in the JVM registers a log-watcher appender under the same + name in the shared log4j config, so a later cluster's watcher can be blind; + the events-viewer test detects this via the API and skips itself. +- The shared menu code logs a benign + `TypeError: Cannot read properties of null (reading 'name')` from + `$scope.showCore` while the per-collection menu resolves (filtered in the + console-error assertion; candidate for a JIRA). - The core overview ping widget answers 503 when the configset has no - healthcheck file; allowed in the smoke test. + healthcheck file (allowed in the affected tests). +- The Schema Designer's backend transiently fails its own prep/analyze calls + with "version mismatch, retry" and recovers via its retry dialog; its API + errors are excluded from the console-error assertion. - ASF Jenkins has no Chrome, so these tests skip there; they run on developer machines and could run in a GitHub Actions workflow (Chrome preinstalled on `ubuntu-latest`) as a follow-up. From 957c0ee610c8947c6eb196f781bfdb5f4fae6f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:05:31 +0200 Subject: [PATCH 19/30] Admin UI tests: stabilize designer/logging tests, track UI bugs in plan doc Marks AdminUiSchemaDesignerTest @AwaitsFix (the designer backend is too flaky under automation), fixes the logging test's logger declaration, and adds a 'Possible UI bugs to investigate' section to the plan doc tracking the issues these tests surfaced, with the workarounds used. --- dev-docs/admin-ui-tests.md | 51 ++++++++++++++++++- solr/webapp/build.gradle | 9 +++- .../solr/webapp/AdminUiLoggingScreenTest.java | 6 ++- .../webapp/AdminUiSchemaDesignerTest.java | 49 ++++++++++-------- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md index 3a27d58281e3..0a46315bf6d4 100644 --- a/dev-docs/admin-ui-tests.md +++ b/dev-docs/admin-ui-tests.md @@ -120,8 +120,10 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI - [x] Add a user through the dialog (verified via `/admin/authentication`) - [ ] Add role / add permission dialogs -### Schema Designer — `AdminUiSchemaDesignerTest` (`@Nightly`) -- [x] Create a new schema, paste a sample doc, analyze; derived field shown +### Schema Designer — `AdminUiSchemaDesignerTest` (`@Nightly`, `@AwaitsFix`) +- [x] Create a new schema, paste a sample doc, analyze; derived field shown — + but the designer backend is too flaky under automation (see Possible UI + bugs), so the test awaits a fix before running by default ## Deliberately skipped (effort vs value) @@ -137,6 +139,51 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI rename/swap/unload need a standalone harness (`JettySolrRunner` without ZK); the cloud harness covers everything else. +## Possible UI bugs to investigate + +Issues surfaced by these tests that look like real bugs, weaknesses or +flakiness in the Admin UI (or its backing APIs) rather than bad test code. +Tests work around them as noted; each deserves investigation and possibly a +JIRA: + +1. **Menu TypeError on per-collection pages**: navigating to any + per-collection screen intermittently logs + `TypeError: Cannot read properties of null (reading 'name')` from + `$scope.showCore` in `js/angular/app.js` — the core selector fires its + change handler with a null core while the menu resolves. Workaround: the + console-error assertion filters this signature. +2. **Collections screen dies without the js-client bundle**: the + `CollectionsV2` service factory (`services.js`) references the `solrApi` + global at injection time; if `libs/solr/index.js` fails to load, the whole + `CollectionsController` fails and the screen is blank. Only + `reloadCollection` is used from that bundle — a lazy/optional lookup would + degrade gracefully. Workaround: tests serve a stub bundle. +3. **Security screen dialogs unreliable under automation**: native clicks on + the Add User toggle and keystrokes into the absolutely-positioned dialog + (jQuery-positioned, `escape-pressed` directive) are dropped in headless + Chrome even though the same interactions work on other screens. May + indicate a focus/z-index issue. Workaround: the test drives the dialog via + the Angular controller scope. +4. **Schema Designer races itself**: creating a schema and analyzing sample + docs transiently fails with `Failed to persist managed schema ... version + mismatch, retry` from its own `prep`/`analyze` calls, surfacing an error + dialog the user has to dismiss. Workaround: the test retries via the + offered Reload Schema button and ignores the designer's own 5xx console + errors. +5. **Plugins screen 500s when metrics are disabled**: `/admin/metrics` with + `wt=prometheus` returns HTTP 500 ("No metrics found in response") when + metrics collection is disabled, instead of a clean error; the Plugins + screen just shows nothing while the console logs the 500. Workaround: + tests enable `metricsEnabled`. +6. **Core overview ping widget logs a 503**: with a configset that has no + healthcheck file, the ping status call answers 503 and the console shows a + resource-load error on every visit; the widget could handle "healthcheck + not configured" gracefully. Workaround: allowed in the affected tests. +7. **Reload success indicator is a 1-second flash**: the Collections screen's + reload button only flags success via a CSS class for one second, which is + easy to miss (and impossible to assert on reliably). Workaround: the test + verifies the reload via the core start time instead. + ## Known limitations - The generated js-client bundle (`libs/solr/index.js`) only exists inside the diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle index eea5e76b8a92..dee1955b5949 100644 --- a/solr/webapp/build.gradle +++ b/solr/webapp/build.gradle @@ -23,8 +23,13 @@ plugins { description = 'Solr webapp' ext { - // The Selenium-based Admin UI tests spawn external chromedriver/Chrome processes, - // which the security manager forbids + // The Selenium-based Admin UI tests execute external binaries (selenium-manager, + // chromedriver) and probe well-known browser install locations plus $PATH and + // $CHROME_BIN for a Chrome binary. Expressing this in the shared solr-tests.policy + // would require FilePermission execute grants on arbitrary, machine-dependent paths + // (effectively <> execute), weakening the sandbox for every test module. + // Following the precedent of the extraction and s3-repository modules, the security + // manager is disabled for this module's tests instead. useSecurityManager = false } diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java index c96404874c74..cc7cef1d16a1 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java @@ -16,6 +16,7 @@ */ package org.apache.solr.webapp; +import java.lang.invoke.MethodHandles; import java.util.List; import java.util.Map; import org.apache.solr.common.util.NamedList; @@ -23,11 +24,14 @@ import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Tests the Logging screens: the recent-events viewer and the log level editor. */ public class AdminUiLoggingScreenTest extends AdminUiTestBase { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @Test public void testLoggingLevelTree() { openPage("~logging/level", By.id("loggingtree")); @@ -40,7 +44,7 @@ public void testLoggingLevelTree() { public void testEventsViewerShowsWarnings() throws Exception { // the cluster nodes run in this JVM, so the log watcher observes our own log events String probeMessage = "Admin UI logging viewer probe event"; - LoggerFactory.getLogger(AdminUiLoggingScreenTest.class).warn(probeMessage); + log.warn(probeMessage); // all test clusters in this JVM register a log-watcher appender under the same name // in the shared log4j config, so this cluster's watcher may be blind when other UI diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java index c90021a0fa00..3ddbe52ae89d 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java @@ -16,6 +16,7 @@ */ package org.apache.solr.webapp; +import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.tests.util.LuceneTestCase.Nightly; import org.junit.Test; import org.openqa.selenium.By; @@ -26,8 +27,12 @@ * let the designer analyze it. * *

Nightly: the designer chains many requests and is the most complex screen in the UI. + * AwaitsFix: the designer backend transiently fails its own prep/analyze calls ("version mismatch, + * retry", "Error loading solr config") when driven at automation speed, making this test flaky even + * with retries; see the "Possible UI bugs" section in dev-docs/admin-ui-tests.md. */ @Nightly +@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-8474") public class AdminUiSchemaDesignerTest extends AdminUiTestBase { @Test @@ -47,27 +52,29 @@ public void testDesignSchemaFromSampleDocument() throws Exception { sampleDocs.sendKeys("[{\"id\":\"1\",\"designer_title\":\"Hello Designer\"}]"); waitFor(By.id("analyze")).click(); - // the analyzed schema lists the field derived from the sample doc; the designer - // occasionally races itself persisting the schema ("version mismatch, retry") - in - // that case dismiss via the offered Reload Schema button and analyze again - waitUntil( - "analyzed schema should list the sample doc field", - () -> { - if (driver.getPageSource().contains("designer_title")) { - return true; - } - if (driver.getPageSource().contains("version mismatch")) { - driver.findElements(By.xpath("//button[contains(., 'Reload Schema')]")).stream() - .filter(WebElement::isDisplayed) - .findFirst() - .ifPresent(WebElement::click); - driver.findElements(By.id("analyze")).stream() - .filter(WebElement::isDisplayed) - .findFirst() - .ifPresent(WebElement::click); - } - return false; - }); + // the analyzed schema lists the field derived from the sample doc. The designer + // backend transiently fails its own calls ("version mismatch, retry", "Error + // loading solr config") and surfaces an error dialog - dismiss it and analyze + // again, with a generous budget since each round trips several requests + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.multipliedBy(3).toNanos(); + boolean analyzed = false; + while (!analyzed && System.nanoTime() < deadlineNanos) { + analyzed = driver.getPageSource().contains("designer_title"); + if (!analyzed) { + for (String dismissButton : new String[] {"Reload Schema", "OK"}) { + driver.findElements(By.xpath("//button[contains(., '" + dismissButton + "')]")).stream() + .filter(WebElement::isDisplayed) + .findFirst() + .ifPresent(WebElement::click); + } + driver.findElements(By.id("analyze")).stream() + .filter(WebElement::isDisplayed) + .findFirst() + .ifPresent(WebElement::click); + Thread.sleep(500); + } + } + assertTrue("Analyzed schema should list the sample doc field", analyzed); // the designer's own API calls (prep/analyze/luke against its temp core) error // transiently while it persists and reloads the schema - it recovers via its retry // dialog, so only unrelated console errors fail the test From 08b215df5acaf4b6bb53d0f7bdab5319647b0d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 20/30] Admin UI tests: query screen paramsets dropdown, edismax toggles, raw params --- .../solr/webapp/AdminUiQueryScreenTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java index 11796e0fd0df..e590419734a6 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiQueryScreenTest.java @@ -17,7 +17,11 @@ package org.apache.solr.webapp; import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.client.solrj.request.RequestWriter; import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.params.CommonParams; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; @@ -79,6 +83,73 @@ public void testRowsAndFieldListParameters() { assertNoSevereConsoleErrors(); } + @Test + public void testParamsetDropdown() throws Exception { + // create a paramset via the API, then apply it through the useParams dropdown + String paramset = "uiqueryparams"; + GenericSolrRequest setParams = + new GenericSolrRequest( + SolrRequest.METHOD.POST, "/" + COLLECTION + "/config/params", params()); + setParams.setContentWriter( + new RequestWriter.StringPayloadContentWriter( + "{\"set\":{\"" + paramset + "\":{\"rows\":\"2\"}}}", CommonParams.JSON_MIME)); + try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { + client.request(setParams); + } + + openPage(COLLECTION + "/query", By.id("query")); + chosenSelect("useParams", paramset); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + + String response = + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); + assertEquals( + "Paramset rows=2 should limit returned docs: " + response, + 2, + countOccurrences(response, "\"id\":")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testEdismaxToggle() { + openPage(COLLECTION + "/query", By.id("query")); + + // the qf field only shows once a dismax parser is selected + assertFalse( + "qf should be hidden for the default parser", + driver.findElement(By.id("qf")).isDisplayed()); + waitFor(By.id("defType")).sendKeys("edismax"); + WebElement qf = waitFor(By.id("qf")); + qf.sendKeys("title_txt"); + WebElement queryInput = waitFor(By.id("q")); + queryInput.clear(); + queryInput.sendKeys("number"); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + + // all fixture docs match "number" in title_txt via the edismax qf + String response = + waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":" + NUM_DOCS); + assertTrue("Response should echo defType", response.contains("edismax")); + + // the edismax-only uf field is offered, dismax hides it again + assertTrue("uf should show for edismax", driver.findElement(By.id("uf")).isDisplayed()); + waitFor(By.id("defType")).sendKeys("dismax"); + assertFalse("uf should hide for dismax", driver.findElement(By.id("uf")).isDisplayed()); + assertNoSevereConsoleErrors(); + } + + @Test + public void testRawQueryParameters() { + openPage(COLLECTION + "/query", By.id("query")); + + waitFor(By.cssSelector("#custom_parameters input[name=rawParamQuery]")).sendKeys("fq=id:2"); + waitFor(By.cssSelector("#query button[type=submit]")).click(); + + String response = waitForTextContains(By.cssSelector("#query #response"), "\"numFound\":1"); + assertTrue("The raw fq param should be echoed: " + response, response.contains("id:2")); + assertNoSevereConsoleErrors(); + } + private static int countOccurrences(String haystack, String needle) { int count = 0; int idx = 0; From c20f68eec31f6766f89e38805bb62249eeb7402b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 21/30] Admin UI tests: security screen add-role and add-permission dialogs Also lifts the class out of Nightly - it runs fast enough for the default suite - and moves the security.json setup from a static block to @BeforeClass, since test runners may load classes long before their suite executes. --- .../solr/webapp/AdminUiSecurityAuthTest.java | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java index 14347e5580e6..da71832e4384 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java @@ -16,12 +16,13 @@ */ package org.apache.solr.webapp; +import java.util.List; import java.util.Map; -import org.apache.lucene.tests.util.LuceneTestCase.Nightly; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.request.GenericSolrRequest; import org.apache.solr.common.util.NamedList; +import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; @@ -30,18 +31,15 @@ /** * Tests the Admin UI with BasicAuth enabled: the login screen flow and the Security screen, * including adding a user through the UI dialog. - * - *

Nightly: the login/session interplay between the browser and the auth filter is the most - * timing-sensitive part of the UI test suite. */ -@Nightly public class AdminUiSecurityAuthTest extends AdminUiTestBase { private static final String USER = "solr"; private static final String PASS = "SolrRocks"; - static { - // consumed by AdminUiTestBase when starting the cluster + @BeforeClass + public static void setSecurityConfig() { + // consumed by AdminUiTestBase when the cluster starts lazily on first use securityJson = "{\n" + " \"authentication\": {\n" @@ -62,8 +60,7 @@ public class AdminUiSecurityAuthTest extends AdminUiTestBase { @Test public void testLoginAndSecurityScreen() throws Exception { // an unauthenticated visit is redirected to the login screen - driver.get(baseUrl + "/index.html#/"); - waitFor(By.id("login")); + openPage("", By.id("login")); WebElement username = waitFor(By.id("username")); username.clear(); username.sendKeys(USER); @@ -105,6 +102,63 @@ public void testLoginAndSecurityScreen() throws Exception { waitUntil("user " + newUser + " should exist", () -> userExists(newUser)); // the users list refreshes to include the new user waitForPageContains(newUser); + + // add a role for the new user through the role dialog + String newRole = "uitestrole"; + waitFor(By.id("add-role")); + ((JavascriptExecutor) driver) + .executeScript( + "var scope = angular.element(document.getElementById('add-role')).scope();" + + " scope.showAddRoleDialog();" + + " scope.upsertRole = {name: arguments[0], selectedUsers: [arguments[1]]};" + + " scope.doUpsertRole(); scope.$apply();", + newRole, + newUser); + waitUntil( + "user " + newUser + " should have role " + newRole, + () -> authorizationApi().toString().contains(newRole)); + waitForPageContains(newRole); + + // grant a predefined permission to the role through the permission dialog + String permission = "collection-admin-read"; + waitFor(By.id("add-permission")); + ((JavascriptExecutor) driver) + .executeScript( + "var scope = angular.element(document.getElementById('add-permission')).scope();" + + " scope.showAddPermDialog();" + + " scope.selectedPredefinedPermission = arguments[0];" + + " scope.upsertPerm.selectedRoles = [arguments[1]];" + + " scope.doUpsertPermission(); scope.$apply();", + permission, + newRole); + waitUntil( + "permission " + permission + " should be granted to " + newRole, + () -> permissionRole(permission).contains(newRole)); + waitForPageContains(permission); + } + + /** Returns the authorization config as fetched with credentials. */ + private NamedList authorizationApi() { + try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { + GenericSolrRequest req = + new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/authorization", params()); + req.setBasicAuthCredentials(USER, PASS); + return client.request(req); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** Returns the roles granted the named permission, as a string, or empty when absent. */ + @SuppressWarnings("unchecked") + private String permissionRole(String permission) { + Map authorization = (Map) authorizationApi().get("authorization"); + for (Map perm : (List>) authorization.get("permissions")) { + if (permission.equals(perm.get("name")) && perm.get("role") != null) { + return perm.get("role").toString(); + } + } + return ""; } /** Checks via the authentication API (with credentials) whether the user exists. */ From f3c1d6ce5487dd7d0a40caa81118aa0a1d8dbffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 22/30] Admin UI tests: SQL screen, with the sql module as a test dependency The sql module jars are already licensed in solr/licenses, so this only adds lockfile entries. Also wires all Test-type tasks to the test sourceSet so the beast task works for this war project, and forwards the Chrome-binary override to all of them. --- solr/webapp/build.gradle | 9 ++- solr/webapp/gradle.lockfile | 17 +++++- .../solr/webapp/AdminUiSqlScreenTest.java | 60 +++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle index dee1955b5949..76dfe22ebf71 100644 --- a/solr/webapp/build.gradle +++ b/solr/webapp/build.gradle @@ -59,6 +59,8 @@ dependencies { testImplementation project(':solr:core') testImplementation project(':solr:solrj') testImplementation project(':solr:test-framework') + // puts the /sql handler on the in-JVM server classpath so the SQL screen works + testRuntimeOnly project(':solr:modules:sql') testImplementation libs.carrotsearch.randomizedtesting.runner testImplementation libs.eclipse.jetty.ee10.servlet testImplementation libs.jakarta.servlet.api @@ -74,8 +76,13 @@ dependencies { }) } -// Forward the browser-binary override for the Admin UI tests to the forked test JVM +// Configure all Test tasks (including the generated beast test_N tasks, which do not +// inherit the default test suite wiring in this war project): +// - test classes/classpath so `gradlew -p solr/webapp beast -Ptests.dups=N` works +// - forward the browser-binary override for the Admin UI tests to the forked test JVM tasks.withType(Test).configureEach { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath def chromeBinary = providers.systemProperty('tests.ui.chrome.binary').orNull if (chromeBinary != null) { systemProperty 'tests.ui.chrome.binary', chromeBinary diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile index fe5a4e3e3206..2809adaeac87 100644 --- a/solr/webapp/gradle.lockfile +++ b/solr/webapp/gradle.lockfile @@ -27,7 +27,8 @@ com.google.guava:failureaccess:1.0.3=annotationProcessor,errorprone,jarValidatio com.google.guava:guava:33.6.0-jre=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,errorprone,jarValidation,solrCore,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.protobuf:protobuf-java:4.35.1=annotationProcessor,errorprone,testAnnotationProcessor +com.google.protobuf:protobuf-java:4.35.1=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testRuntimeClasspath +com.googlecode.json-simple:json-simple:1.1.1=jarValidation,testRuntimeClasspath com.j256.simplemagic:simplemagic:1.17=jarValidation,solrCore,testRuntimeClasspath com.jayway.jsonpath:json-path:3.0.0=jarValidation,solrCore,testRuntimeClasspath com.lmax:disruptor:4.0.0=serverLib @@ -80,12 +81,20 @@ javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcess junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.18.11=jarValidation,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.2=jarValidation,solrCore,testRuntimeClasspath +org.apache.calcite.avatica:avatica-core:1.25.0=jarValidation,testRuntimeClasspath +org.apache.calcite.avatica:avatica-metrics:1.25.0=jarValidation,testRuntimeClasspath +org.apache.calcite:calcite-core:1.37.0=jarValidation,testRuntimeClasspath +org.apache.calcite:calcite-linq4j:1.37.0=jarValidation,testRuntimeClasspath org.apache.commons:commons-exec:1.6.0=jarValidation,solrCore,testRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=jarValidation,solrCore,testRuntimeClasspath org.apache.commons:commons-math3:3.6.1=jarValidation,solrCore,testRuntimeClasspath +org.apache.commons:commons-text:1.15.0=jarValidation,testRuntimeClasspath org.apache.curator:curator-client:5.9.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath org.apache.curator:curator-framework:5.9.0=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath org.apache.curator:curator-test:5.9.0=jarValidation,testRuntimeClasspath +org.apache.httpcomponents.client5:httpclient5:5.2.1=jarValidation,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5-h2:5.2=jarValidation,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5:5.2.3=jarValidation,testRuntimeClasspath org.apache.logging.log4j:log4j-1.2-api:2.26.0=serverLib org.apache.logging.log4j:log4j-api:2.26.0=jarValidation,serverLib,solrCore,testRuntimeClasspath org.apache.logging.log4j:log4j-core:2.26.0=jarValidation,serverLib,solrCore,testRuntimeClasspath @@ -117,6 +126,9 @@ org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspat org.apache.zookeeper:zookeeper-jute:3.9.5=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath org.apache.zookeeper:zookeeper:3.9.5=jarValidation,solrCore,testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath +org.checkerframework:checker-qual:4.2.0=jarValidation,testRuntimeClasspath +org.codehaus.janino:commons-compiler:3.1.11=jarValidation,testRuntimeClasspath +org.codehaus.janino:janino:3.1.11=jarValidation,testRuntimeClasspath org.codehaus.woodstox:stax2-api:4.3.0=jarValidation,solrCore,testRuntimeClasspath org.eclipse.jetty.compression:jetty-compression-common:12.1.10=jarValidation,solrCore,testRuntimeClasspath org.eclipse.jetty.compression:jetty-compression-gzip:12.1.10=jarValidation,solrCore,testRuntimeClasspath @@ -163,6 +175,9 @@ org.jspecify:jspecify:1.0.0=annotationProcessor,errorprone,jarValidation,solrCor org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath +org.locationtech.jts.io:jts-io-common:1.19.0=jarValidation,testRuntimeClasspath +org.locationtech.jts:jts-core:1.19.0=jarValidation,testRuntimeClasspath +org.locationtech.proj4j:proj4j:1.2.2=jarValidation,testRuntimeClasspath org.locationtech.spatial4j:spatial4j:0.8=jarValidation,solrCore,testRuntimeClasspath org.opentest4j:opentest4j:1.2.0=jarValidation,testRuntimeClasspath org.ow2.asm:asm-commons:9.10.1=jarValidation,solrCore,testRuntimeClasspath diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java new file mode 100644 index 000000000000..5c82212738ff --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java @@ -0,0 +1,60 @@ +/* + * 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.webapp; + +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.common.SolrInputDocument; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +/** + * Tests the SQL screen: executing a SQL query through the form. Requires the sql module on the + * server classpath, provided by the webapp test dependencies. + */ +public class AdminUiSqlScreenTest extends AdminUiTestBase { + + private static final String COLLECTION = "sqlcoll"; + + @BeforeClass + public static void setupCollection() throws Exception { + createFixtureCollection(COLLECTION, 1, 1); + SolrClient client = cluster.getSolrClient(COLLECTION); + for (int i = 1; i <= 3; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "sql-doc-" + i); + client.add(doc); + } + client.commit(); + } + + @Test + public void testSqlQueryViaUi() { + openPage(COLLECTION + "/sqlquery", By.id("sqlquery")); + WebElement stmt = waitFor(By.id("sqlexp")); + stmt.clear(); + stmt.sendKeys("SELECT id FROM " + COLLECTION + " LIMIT 10"); + waitFor(By.xpath("//div[@id='sqlquery']//button[@type='submit']")).click(); + + // the result grid lists all documents + for (int i = 1; i <= 3; i++) { + waitForPageContains("sql-doc-" + i); + } + assertNoSevereConsoleErrors(); + } +} From 852ef5deca34acdd94227f84d1ed73861c62eefa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 23/30] Admin UI tests: standalone-mode harness and Core Admin write actions The cloud cluster now starts lazily on first use so subclasses can configure security.json or standalone mode in @BeforeClass; static-block configuration was order-dependent because test runners may load all classes up front. AdminUiStandaloneTestBase builds a no-ZooKeeper solr home and starts a standalone JettySolrRunner serving the UI. AdminUiCoreAdminStandaloneTest covers the standalone-only core admin actions - add, rename, swap (verified by the indexes exchanging) and unload (accepting the native confirm dialog) - plus the standalone menu differences. Adds a stale-safe setText helper for Angular-re-rendered form fields. --- .../AdminUiCoreAdminStandaloneTest.java | 145 ++++++++++++++++++ .../apache/solr/webapp/AdminUiSmokeTest.java | 7 +- .../webapp/AdminUiStandaloneTestBase.java | 91 +++++++++++ .../apache/solr/webapp/AdminUiTestBase.java | 92 +++++++++-- 4 files changed, 312 insertions(+), 23 deletions(-) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminStandaloneTest.java create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiStandaloneTestBase.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminStandaloneTest.java new file mode 100644 index 000000000000..1ebcee9bed4e --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiCoreAdminStandaloneTest.java @@ -0,0 +1,145 @@ +/* + * 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.webapp; + +import java.nio.file.Path; +import java.util.Map; +import org.apache.solr.client.solrj.request.SolrQuery; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.util.NamedList; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Tests the Core Admin screen's write actions - add, rename, swap and unload - which only apply to + * a standalone (user-managed) Solr node; in cloud mode these operations belong to the Collections + * API. Also asserts the standalone-mode differences of the UI menus. + */ +public class AdminUiCoreAdminStandaloneTest extends AdminUiStandaloneTestBase { + + private static Path home; + + @BeforeClass + public static void startStandaloneNode() throws Exception { + home = buildStandaloneHome("renamecore", "swapa", "swapb", "unloadcore"); + // instance dir for the add-core test; the core is created through the UI + createStandaloneCoreDir(home, "addedcore"); + standaloneJetty = startStandaloneJetty(home); + baseUrl = standaloneJetty.getBaseUrl().toString(); + } + + @AfterClass + public static void stopStandaloneNode() throws Exception { + if (standaloneJetty != null) { + standaloneJetty.stop(); + standaloneJetty = null; + } + } + + @Test + public void testStandaloneMenus() { + openPage("", By.id("index")); + // cloud-only menu entries are absent in standalone mode + assertTrue(driver.findElements(By.cssSelector("#menu .cloud")).isEmpty()); + assertTrue(driver.findElements(By.cssSelector("#menu .collections")).isEmpty()); + // the per-core menu (shown when a core page is open) offers the core-level + // screens directly - query and replication are standalone-only entries + openPage("swapa/core-overview", By.id("dashboard")); + waitFor(By.cssSelector("#core-menu .query")); + waitFor(By.cssSelector("#core-menu .replication")); + // the ping widget answers 503 when the configset has no healthcheck file + assertNoSevereConsoleErrors("/admin/ping"); + } + + @Test + public void testAddCoreViaUi() throws Exception { + openPage("~cores", By.id("cores")); + waitFor(By.cssSelector("#cores #add")).click(); + setText(By.id("add_name"), "addedcore"); + setText(By.id("add_instanceDir"), "addedcore"); + waitFor(By.xpath("//button[@ng-click='addCore()']")).click(); + + waitUntil("core addedcore should exist", () -> coreExists("addedcore")); + waitForPageContains("addedcore"); + assertNoSevereConsoleErrors(); + } + + @Test + public void testRenameCoreViaUi() throws Exception { + openPage("~cores/renamecore", By.id("cores")); + waitFor(By.cssSelector("#cores #rename")).click(); + setText(By.id("rename_other"), "renamedcore"); + waitFor(By.xpath("//button[@ng-click='renameCore()']")).click(); + + waitUntil( + "core should be renamed", () -> coreExists("renamedcore") && !coreExists("renamecore")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testSwapCoresViaUi() throws Exception { + // make the cores distinguishable: swapa gets one document, swapb stays empty + try (var client = standaloneJetty.newClient()) { + var doc = new SolrInputDocument(); + doc.addField("id", "swap-doc"); + client.add("swapa", doc); + client.commit("swapa"); + } + assertEquals(1, numDocs("swapa")); + assertEquals(0, numDocs("swapb")); + + openPage("~cores/swapa", By.id("cores")); + waitFor(By.cssSelector("#cores #swap")).click(); + // pick the other core in the swap-with dropdown (plain select) + waitFor(By.id("swap_other")).sendKeys("swapb"); + waitFor(By.xpath("//button[@ng-click='swapCores()']")).click(); + + waitUntil("swap should exchange the cores' indexes", () -> numDocs("swapb") == 1); + assertEquals(0, numDocs("swapa")); + assertNoSevereConsoleErrors(); + } + + @Test + public void testUnloadCoreViaUi() throws Exception { + openPage("~cores/unloadcore", By.id("cores")); + waitFor(By.cssSelector("#cores #unload")).click(); + // unload asks for confirmation via a native browser dialog + driver.switchTo().alert().accept(); + + waitUntil("core unloadcore should be gone", () -> !coreExists("unloadcore")); + assertNoSevereConsoleErrors(); + } + + private boolean coreExists(String coreName) { + try { + NamedList response = adminApi("/admin/cores", params()); + return ((Map) response.get("status")).containsKey(coreName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private long numDocs(String coreName) { + try (var client = standaloneJetty.newClient()) { + return client.query(coreName, new SolrQuery("*:*")).getResults().getNumFound(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java index badd47151a63..5dfc5b2a4517 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSmokeTest.java @@ -17,8 +17,6 @@ package org.apache.solr.webapp; import java.util.Map; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.util.ExternalPaths; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; @@ -33,10 +31,7 @@ public class AdminUiSmokeTest extends AdminUiTestBase { @BeforeClass public static void setupCollection() throws Exception { - cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, COLLECTION); - CollectionAdminRequest.createCollection(COLLECTION, COLLECTION, 1, 2) - .process(cluster.getSolrClient()); - cluster.waitForActiveCollection(COLLECTION, 1, 2); + createFixtureCollection(COLLECTION, 1, 2); } @Test diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStandaloneTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStandaloneTestBase.java new file mode 100644 index 000000000000..0d18b3939a34 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStandaloneTestBase.java @@ -0,0 +1,91 @@ +/* + * 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.webapp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.util.ExternalPaths; +import org.junit.BeforeClass; + +/** + * Base class for Admin UI tests of a standalone (user-managed, no ZooKeeper) Solr node, whose UI + * differs from cloud mode: no Cloud/Collections/Schema Designer menus, and the per-core menu offers + * analysis, documents, query, replication etc. directly. + * + *

The base {@code @BeforeClass} only starts the browser (via {@link #standaloneMode}); this + * class builds a solr home and starts a standalone {@link JettySolrRunner} serving the UI. + */ +public abstract class AdminUiStandaloneTestBase extends AdminUiTestBase { + + /** + * Arms standalone mode before the cloud cluster can start lazily. Runs after the base class's + * browser-starting {@code @BeforeClass} and before any subclass {@code @BeforeClass} or test. + */ + @BeforeClass + public static void enableStandaloneMode() { + standaloneMode = true; + } + + /** Builds a solr home with the shared test solr.xml and the given pre-created cores. */ + protected static Path buildStandaloneHome(String... coreNames) throws IOException { + Path home = createTempDir("standalone-home"); + Files.copy( + ExternalPaths.SOURCE_HOME.resolve("core/src/test-files/solr/solr.xml"), + home.resolve("solr.xml")); + for (String coreName : coreNames) { + createStandaloneCoreDir(home, coreName); + Properties props = new Properties(); + props.setProperty("name", coreName); + writeCoreProperties(home.resolve(coreName), props, coreName); + } + return home; + } + + /** Creates a core instance dir with the default configset, without registering the core. */ + protected static void createStandaloneCoreDir(Path home, String coreName) throws IOException { + Path confDir = home.resolve(coreName).resolve("conf"); + Files.createDirectories(confDir); + copyDirectory(ExternalPaths.DEFAULT_CONFIGSET, confDir); + } + + /** Starts a standalone Jetty on the given home, serving the Admin UI. */ + protected static JettySolrRunner startStandaloneJetty(Path home) throws Exception { + JettyConfig.Builder config = JettyConfig.builder(); + configureJettyForUi(config); + JettySolrRunner jetty = new JettySolrRunner(home.toString(), new Properties(), config.build()); + jetty.start(); + return jetty; + } + + protected static void copyDirectory(Path source, Path target) throws IOException { + try (var paths = Files.walk(source)) { + for (Path path : (Iterable) paths::iterator) { + Path dest = target.resolve(source.relativize(path).toString()); + if (Files.isDirectory(path)) { + Files.createDirectories(dest); + } else { + Files.createDirectories(dest.getParent()); + Files.copy(path, dest); + } + } + } + } +} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index fbecd162246f..2cbe648a48ea 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -46,6 +46,8 @@ import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; import org.apache.solr.util.ExternalPaths; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.junit.AfterClass; @@ -106,11 +108,23 @@ public abstract class AdminUiTestBase extends SolrCloudTestCase { protected static String baseUrl; /** - * Optional security.json for the cluster. Subclasses must assign this in a {@code static} block - * (which runs before this class's cluster-starting {@code @BeforeClass} method). + * Optional security.json for the cluster. Subclasses assign this in their {@code @BeforeClass} + * (which runs after this class's browser-starting one, but before the cluster starts lazily on + * first use). Never assign it in a {@code static} block: test runners may load all test classes + * up front, so static initializers of one class can run long before its suite executes. */ protected static String securityJson; + /** + * When true (set by {@code AdminUiStandaloneTestBase}), no cloud cluster is started; the test + * class starts its own standalone {@link JettySolrRunner}(s), assigns {@link #standaloneJetty} + * and {@link #baseUrl}, and stops them again. + */ + protected static boolean standaloneMode = false; + + /** The standalone node backing {@link #adminApi} when {@link #standaloneMode} is set. */ + protected static JettySolrRunner standaloneJetty; + /** * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), * which only exists inside the built webapp, not in the source tree tests serve from. The @@ -171,20 +185,8 @@ public static void startClusterAndBrowser() throws Exception { // metrics are off by default in test clusters, but UI screens (e.g. Plugins) need them; // restored after the class by SolrTestCase's SystemPropertiesRestoreRule System.setProperty("metricsEnabled", "true"); - var clusterBuilder = - configureCluster(2) - .withJettyConfig( - jetty -> - jetty - .enableAdminUi(true) - // exact-path mapping takes precedence over the static /libs/* servlet - .withServlet( - new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js")); - if (securityJson != null) { - clusterBuilder.withSecurityJson(securityJson); - } - clusterBuilder.configure(); - baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); + // the cluster starts lazily via ensureCloudCluster(), after subclass @BeforeClass + // methods have had the chance to configure securityJson or standalone mode ChromeOptions options = new ChromeOptions(); options.setBinary(chrome.toString()); @@ -206,8 +208,39 @@ public static void startClusterAndBrowser() throws Exception { driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30)); } + /** Starts the 2-node cloud cluster serving the UI, unless already started. */ + protected static void ensureCloudCluster() { + if (standaloneMode || cluster != null) { + return; + } + try { + var clusterBuilder = + configureCluster(2).withJettyConfig(AdminUiTestBase::configureJettyForUi); + if (securityJson != null) { + clusterBuilder.withSecurityJson(securityJson); + } + clusterBuilder.configure(); + baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); + } catch (Exception e) { + throw new RuntimeException("Could not start UI test cluster", e); + } + } + + /** Configures a Jetty node to serve the Admin UI plus the js-client stub. */ + protected static void configureJettyForUi(JettyConfig.Builder jetty) { + jetty + .enableAdminUi(true) + // exact-path mapping takes precedence over the static /libs/* servlet + .withServlet(new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js"); + } + @AfterClass public static void stopBrowser() { + // reset the static per-class configuration: several test classes run in the same + // JVM, and flags set by one class's static initializer must not leak into the next + standaloneMode = false; + standaloneJetty = null; + securityJson = null; if (driver != null) { try { driver.quit(); @@ -251,6 +284,7 @@ protected void failed(Throwable e, Description description) { * @return the anchor element */ protected static WebElement openPage(String route, By anchor) { + ensureCloudCluster(); driver.get(baseUrl + "/index.html#/" + route); return waitFor(anchor); } @@ -306,7 +340,9 @@ private static T poll(By locator, Function condition, String */ protected static NamedList adminApi(String path, SolrParams params) throws IOException, SolrServerException { - try (SolrClient client = cluster.getJettySolrRunner(0).newClient()) { + ensureCloudCluster(); + JettySolrRunner jetty = standaloneMode ? standaloneJetty : cluster.getJettySolrRunner(0); + try (SolrClient client = jetty.newClient()) { return client.request(new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)); } } @@ -318,6 +354,7 @@ protected static NamedList adminApi(String path, SolrParams params) */ protected static void createFixtureCollection(String name, int numShards, int numReplicas) throws Exception { + ensureCloudCluster(); cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, name); CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(name, name, numShards, numReplicas); @@ -351,6 +388,24 @@ protected static String coreNameOnNode0(String collection) { throw new AssertionError("No core found on node 0 for collection " + collection); } + /** + * Clears the input at the locator and types the given text, retrying when Angular re-renders the + * element mid-interaction (StaleElementReferenceException). + */ + protected static void setText(By locator, String text) { + poll( + locator, + el -> { + if (!el.isDisplayed()) { + return null; + } + el.clear(); + el.sendKeys(text); + return Boolean.TRUE; + }, + "typing '" + text + "'"); + } + /** Waits until the element's rendered text contains the given substring, and returns it. */ protected static String waitForTextContains(By locator, String substring) { return poll( @@ -420,6 +475,9 @@ protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { Arrays.stream(allowedSubstrings) .noneMatch(allowed -> entry.getMessage().contains(allowed))) .filter(entry -> !entry.getMessage().contains("favicon.ico")) + // the ui-grid icon font referenced from ui-grid.min.css is not shipped with + // the webapp at all, so it 404s in production too + .filter(entry -> !entry.getMessage().contains("fonts/ui-grid")) // benign race in the shared menu code: showCore() fires with a null core // while the per-collection menu resolves after navigation .filter( From e874a15102a53c54522c3965e07725929561d326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 24/30] Admin UI tests: replication screen actions on a standalone leader/follower pair Starts two standalone JettySolrRunners configured from the replication test configs; disables polling, indexes on the leader, replicates on demand via the UI button and re-enables polling - verified via the replication API. --- .../AdminUiReplicationStandaloneTest.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java new file mode 100644 index 000000000000..ceb86fd77357 --- /dev/null +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java @@ -0,0 +1,154 @@ +/* + * 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.webapp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Properties; +import org.apache.solr.client.solrj.request.SolrQuery; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.util.ExternalPaths; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Tests the Replication screen on a standalone leader/follower pair: the follower's screen shows + * the leader info, and the polling and replicate-now actions work. + */ +public class AdminUiReplicationStandaloneTest extends AdminUiStandaloneTestBase { + + private static final String CORE = "collection1"; + private static final Path REPLICATION_CONF = + ExternalPaths.SOURCE_HOME.resolve("core/src/test-files/solr/collection1/conf"); + + private static JettySolrRunner leaderJetty; + + @BeforeClass + public static void startLeaderAndFollower() throws Exception { + // sets the solr.tests.* index-config properties the test solrconfigs require + newRandomConfig(); + // the follower's leaderUrl is not covered by the URL allow-list + systemSetPropertyEnableUrlAllowList(false); + + Path leaderHome = buildReplicationHome("solrconfig-leader.xml", 0); + leaderJetty = new JettySolrRunner(leaderHome.toString(), JettyConfig.builder().build()); + leaderJetty.start(); + + Path followerHome = buildReplicationHome("solrconfig-follower.xml", leaderJetty.getLocalPort()); + standaloneJetty = startStandaloneJetty(followerHome); + baseUrl = standaloneJetty.getBaseUrl().toString(); + } + + @AfterClass + public static void stopLeaderAndFollower() throws Exception { + if (standaloneJetty != null) { + standaloneJetty.stop(); + standaloneJetty = null; + } + if (leaderJetty != null) { + leaderJetty.stop(); + leaderJetty = null; + } + } + + @Test + public void testReplicationScreenAndActions() throws Exception { + openPage(CORE + "/replication", By.id("replication")); + // the follower screen shows its own and the leader's index version info + waitForPageContains("Version"); + waitForPageContains(":" + leaderJetty.getLocalPort()); + + // disable polling so replication only happens on demand + waitFor(By.cssSelector("#replication button.disable-polling")).click(); + waitUntil( + "polling should be disabled", () -> "true".equals(followerDetail("isPollingDisabled"))); + + // index documents on the leader; the follower does not poll them + try (var client = leaderJetty.newClient()) { + for (int i = 1; i <= 2; i++) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "repl-doc-" + i); + doc.addField("name", "replicated"); + client.add(CORE, doc); + } + client.commit(CORE); + } + + // replicate on demand and watch the docs arrive on the follower + waitFor(By.cssSelector("#replication button.replicate-now")).click(); + waitUntil("follower should receive the docs after replicate-now", () -> followerNumDocs() == 2); + + // re-enable polling + waitFor(By.cssSelector("#replication button.enable-polling")).click(); + waitUntil( + "polling should be enabled again", + () -> "false".equals(followerDetail("isPollingDisabled"))); + assertNoSevereConsoleErrors(); + } + + /** Builds a home with one core configured from the given test solrconfig variant. */ + private static Path buildReplicationHome(String solrconfigName, int leaderPort) + throws IOException { + Path home = createTempDir("replication-home"); + Files.copy( + ExternalPaths.SOURCE_HOME.resolve("core/src/test-files/solr/solr.xml"), + home.resolve("solr.xml")); + Path confDir = home.resolve(CORE).resolve("conf"); + Files.createDirectories(confDir); + String solrconfig = Files.readString(REPLICATION_CONF.resolve(solrconfigName)); + solrconfig = + solrconfig + .replace("TEST_PORT", Integer.toString(leaderPort)) + .replace("COMPRESSION", "internal"); + Files.writeString(confDir.resolve("solrconfig.xml"), solrconfig); + Files.copy(REPLICATION_CONF.resolve("schema-replication1.xml"), confDir.resolve("schema.xml")); + Files.copy( + REPLICATION_CONF.resolve("solrconfig.snippet.randomindexconfig.xml"), + confDir.resolve("solrconfig.snippet.randomindexconfig.xml")); + Properties props = new Properties(); + props.setProperty("name", CORE); + writeCoreProperties(home.resolve(CORE), props, CORE); + return home; + } + + /** Reads a detail from the follower section of the replication details API. */ + private String followerDetail(String key) { + try { + NamedList response = + adminApi("/" + CORE + "/replication", params("command", "details")); + Object value = response._get(List.of("details", "follower", key), null); + return value == null ? "" : value.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private long followerNumDocs() { + try (var client = standaloneJetty.newClient()) { + return client.query(CORE, new SolrQuery("*:*")).getResults().getNumFound(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 2b1aa896863b3a12bc63f8a97907324465c652da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 01:50:16 +0200 Subject: [PATCH 25/30] Admin UI tests: drop Nightly from schema designer test, update plan doc The designer test stays AwaitsFix due to its backend's flakiness. The plan doc reflects the resolved TODOs (query depth, security dialogs, SQL, standalone core admin and replication) and the new findings, including the missing ui-grid icon font. --- dev-docs/admin-ui-tests.md | 52 +++++++++++++------ .../webapp/AdminUiSchemaDesignerTest.java | 9 ++-- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md index 0a46315bf6d4..faa7bfedff82 100644 --- a/dev-docs/admin-ui-tests.md +++ b/dev-docs/admin-ui-tests.md @@ -33,10 +33,13 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI admin APIs — never hardcoded values. - Tests are grouped per screen/feature, so each screen's display and write tests live in the same class. +- Most tests run against a 2-node cloud cluster; `AdminUiStandaloneTestBase` + additionally supports standalone (user-managed, no ZooKeeper) nodes, whose + UI differs (no Cloud/Collections/Schema Designer menus; the per-core menu + offers query/replication etc. directly). - On failure, a screenshot, the page source and the browser console log are saved into the test temp dir. -- Run with: `./gradlew :solr:webapp:test` (add `-Ptests.nightly=true` for the - security and schema-designer classes) +- Run with: `./gradlew :solr:webapp:test` ## Coverage by screen @@ -71,7 +74,10 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI ### Query screen — `AdminUiQueryScreenTest` - [x] `*:*` and `id:` queries via the form, `numFound` in the response - [x] `rows` and `fl` parameters affect the returned documents -- [ ] Paramsets dropdown, dismax/edismax toggles, raw query parameters +- [x] Paramsets dropdown applies a paramset created via the API +- [x] defType dismax/edismax toggles reveal their parameter fields; edismax + query with `qf` returns the expected results +- [x] Raw query parameters (e.g. an extra `fq`) are applied ### Documents screen — `AdminUiDocumentsScreenTest` - [x] Form renders with JSON/XML/CSV document types @@ -93,11 +99,16 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI when the node's log watcher is blind due to shared-JVM log4j state, see Known limitations) -### Core Admin screen — `AdminUiCoreAdminScreenTest` +### Core Admin screen — `AdminUiCoreAdminScreenTest` (cloud) and +### `AdminUiCoreAdminStandaloneTest` (standalone) - [x] Hosted core listed, matching `/admin/cores` - [x] Reload core via the button (success indicator) -- [ ] Add/rename/swap/unload core — cloud-mode core admin operations conflict - with the Overseer; needs a standalone-mode harness +- [x] Standalone-mode menu differences (no cloud menus; core menu offers + query/replication) +- [x] Add core via the dialog (pre-created instance dir) +- [x] Rename core via the dialog +- [x] Swap cores via the dialog (verified by the indexes exchanging) +- [x] Unload core, accepting the native confirm dialog ### Per-collection display screens — `AdminUiCollectionScreensTest` - [x] Analysis: `text_general` tokenizes and lowercases entered text @@ -109,35 +120,37 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI ### Stream screen — `AdminUiStreamScreenTest` - [x] A `search(...)` streaming expression executes and renders all docs -### Replication screen — `AdminUiReplicationScreenTest` +### SQL screen — `AdminUiSqlScreenTest` +- [x] A SQL query executes through the form and the result grid lists the + documents (the sql module is a test-only dependency of `solr:webapp`) + +### Replication screen — `AdminUiReplicationScreenTest` (cloud) and +### `AdminUiReplicationStandaloneTest` (standalone leader/follower) - [x] Renders index version info in cloud mode -- [ ] Standalone leader/follower actions (replicate now, disable polling) — - needs a standalone-mode harness +- [x] Follower screen shows the leader's info +- [x] Disable polling, index on the leader, replicate-now transfers the + index, re-enable polling — verified via the replication API -### Security with BasicAuth — `AdminUiSecurityAuthTest` (`@Nightly`) +### Security with BasicAuth — `AdminUiSecurityAuthTest` - [x] Unauthenticated visit redirects to login; login form authenticates - [x] Security screen shows authn/authz plugins, users, roles, permissions - [x] Add a user through the dialog (verified via `/admin/authentication`) -- [ ] Add role / add permission dialogs +- [x] Add a role for the user through the dialog (verified via API) +- [x] Grant a predefined permission to the role (verified via API) -### Schema Designer — `AdminUiSchemaDesignerTest` (`@Nightly`, `@AwaitsFix`) +### Schema Designer — `AdminUiSchemaDesignerTest` (`@AwaitsFix`) - [x] Create a new schema, paste a sample doc, analyze; derived field shown — but the designer backend is too flaky under automation (see Possible UI bugs), so the test awaits a fix before running by default ## Deliberately skipped (effort vs value) -- **SQL screen**: needs the `sql` module (Calcite and friends) on the webapp - test classpath, dragging in many jars and license files for one screen. - **JWT/OAuth login flows**: require an external identity provider or heavy mocking; BasicAuth covers the UI's login/session mechanics. - **Keystroke-level entry in the security dialogs**: native clicks/keystrokes into the absolutely-positioned dialogs proved unreliable in headless Chrome; the dialogs are driven via the Angular controller scope instead. Keyboard entry is covered by the login form and the other screens' forms. -- **Standalone (non-cloud) mode screens**: replication actions and core admin - rename/swap/unload need a standalone harness (`JettySolrRunner` without ZK); - the cloud harness covers everything else. ## Possible UI bugs to investigate @@ -183,6 +196,11 @@ JIRA: reload button only flags success via a CSS class for one second, which is easy to miss (and impossible to assert on reliably). Workaround: the test verifies the reload via the core start time instead. +8. **ui-grid icon font is missing from the webapp**: `css/angular/ui-grid.min.css` + references `fonts/ui-grid.woff` (and .ttf/.eot), but no such font files are + shipped anywhere under `solr/webapp/web` — the SQL screen's result grid + logs a 404 for it in production too, and grid icons render as boxes. + Workaround: the console-error assertion filters this 404. ## Known limitations diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java index 3ddbe52ae89d..daa0370ec043 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java @@ -17,7 +17,6 @@ package org.apache.solr.webapp; import org.apache.lucene.tests.util.LuceneTestCase; -import org.apache.lucene.tests.util.LuceneTestCase.Nightly; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -26,12 +25,10 @@ * Happy-path test of the Schema Designer screen: create a new schema, paste a sample document and * let the designer analyze it. * - *

Nightly: the designer chains many requests and is the most complex screen in the UI. - * AwaitsFix: the designer backend transiently fails its own prep/analyze calls ("version mismatch, - * retry", "Error loading solr config") when driven at automation speed, making this test flaky even - * with retries; see the "Possible UI bugs" section in dev-docs/admin-ui-tests.md. + *

AwaitsFix: the designer backend transiently fails its own prep/analyze calls ("version + * mismatch, retry", "Error loading solr config") when driven at automation speed, making this test + * flaky even with retries; see the "Possible UI bugs" section in dev-docs/admin-ui-tests.md. */ -@Nightly @LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-8474") public class AdminUiSchemaDesignerTest extends AdminUiTestBase { From 9e8f069571a2e3c9513c51545af111a885eb80bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 02:13:40 +0200 Subject: [PATCH 26/30] Address Copilot review: EnvUtils for sysprop read, start cluster before log probe --- .../test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java | 4 ++++ .../src/test/org/apache/solr/webapp/AdminUiTestBase.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java index cc7cef1d16a1..2fee9698d1d7 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiLoggingScreenTest.java @@ -42,6 +42,10 @@ public void testLoggingLevelTree() { @Test public void testEventsViewerShowsWarnings() throws Exception { + // start the cluster before emitting the probe: the log watcher only exists once the + // nodes are up, and with lazy startup this test may be the first cluster user + ensureCloudCluster(); + // the cluster nodes run in this JVM, so the log watcher observes our own log events String probeMessage = "Admin UI logging viewer probe event"; log.warn(probeMessage); diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java index 2cbe648a48ea..8b36b7967737 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java @@ -45,6 +45,7 @@ import org.apache.solr.client.solrj.request.GenericSolrRequest; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.NamedList; import org.apache.solr.embedded.JettyConfig; import org.apache.solr.embedded.JettySolrRunner; @@ -491,7 +492,7 @@ protected static void assertNoSevereConsoleErrors(String... allowedSubstrings) { /** Locates a Chrome/Chromium binary, or returns null if none can be found. */ @SuppressForbidden(reason = "Reading CHROME_BIN/PATH from the environment to locate a browser") protected static Path findChromeBinary() { - String sysProp = System.getProperty("tests.ui.chrome.binary"); + String sysProp = EnvUtils.getProperty("tests.ui.chrome.binary"); if (sysProp != null) { Path path = Path.of(sysProp); return Files.isExecutable(path) ? path : null; From c6c89adedcbba7c754f335a2056dcc6c61c6d428 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 13 Aug 2026 21:55:05 -0400 Subject: [PATCH 27/30] Less fragile test fixes flakyness. --- .../solr/webapp/AdminUiReplicationStandaloneTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java index ceb86fd77357..f10ddbf51b58 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiReplicationStandaloneTest.java @@ -77,7 +77,12 @@ public void testReplicationScreenAndActions() throws Exception { openPage(CORE + "/replication", By.id("replication")); // the follower screen shows its own and the leader's index version info waitForPageContains("Version"); - waitForPageContains(":" + leaderJetty.getLocalPort()); + // scraping the rendered DOM for the leader's port is flaky: the "leader url:" row + // can lag the rest of the screen's data on the very first load. Assert against the + // API response instead, like the isPollingDisabled/followerNumDocs checks below do. + waitUntil( + "follower should report the leader's url", + () -> followerDetail("leaderUrl").contains(":" + leaderJetty.getLocalPort())); // disable polling so replication only happens on demand waitFor(By.cssSelector("#replication button.disable-polling")).click(); From 975b5ec95bba1cad544c177de81fa233c4acd090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 10:29:44 +0200 Subject: [PATCH 28/30] Indent license-file first line --- solr/licenses/selenium-LICENSE-ASL.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/licenses/selenium-LICENSE-ASL.txt b/solr/licenses/selenium-LICENSE-ASL.txt index d0381d6d04c7..1a9893b43e8e 100644 --- a/solr/licenses/selenium-LICENSE-ASL.txt +++ b/solr/licenses/selenium-LICENSE-ASL.txt @@ -1,4 +1,4 @@ -Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ From 3678753b2596813cb9f1d382c294cb5658817db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 10:28:58 +0200 Subject: [PATCH 29/30] Lift bug section into SOLR-18347 --- dev-docs/admin-ui-tests.md | 55 +++----------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/dev-docs/admin-ui-tests.md b/dev-docs/admin-ui-tests.md index faa7bfedff82..1a881631364e 100644 --- a/dev-docs/admin-ui-tests.md +++ b/dev-docs/admin-ui-tests.md @@ -140,8 +140,9 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI ### Schema Designer — `AdminUiSchemaDesignerTest` (`@AwaitsFix`) - [x] Create a new schema, paste a sample doc, analyze; derived field shown — - but the designer backend is too flaky under automation (see Possible UI - bugs), so the test awaits a fix before running by default + but the designer backend is too flaky under automation + (see [SOLR-18347](https://issues.apache.org/jira/browse/SOLR-18347)), + so the test awaits a fix before running by default ## Deliberately skipped (effort vs value) @@ -152,56 +153,6 @@ This document tracks browser-based test coverage of the old AngularJS Admin UI the dialogs are driven via the Angular controller scope instead. Keyboard entry is covered by the login form and the other screens' forms. -## Possible UI bugs to investigate - -Issues surfaced by these tests that look like real bugs, weaknesses or -flakiness in the Admin UI (or its backing APIs) rather than bad test code. -Tests work around them as noted; each deserves investigation and possibly a -JIRA: - -1. **Menu TypeError on per-collection pages**: navigating to any - per-collection screen intermittently logs - `TypeError: Cannot read properties of null (reading 'name')` from - `$scope.showCore` in `js/angular/app.js` — the core selector fires its - change handler with a null core while the menu resolves. Workaround: the - console-error assertion filters this signature. -2. **Collections screen dies without the js-client bundle**: the - `CollectionsV2` service factory (`services.js`) references the `solrApi` - global at injection time; if `libs/solr/index.js` fails to load, the whole - `CollectionsController` fails and the screen is blank. Only - `reloadCollection` is used from that bundle — a lazy/optional lookup would - degrade gracefully. Workaround: tests serve a stub bundle. -3. **Security screen dialogs unreliable under automation**: native clicks on - the Add User toggle and keystrokes into the absolutely-positioned dialog - (jQuery-positioned, `escape-pressed` directive) are dropped in headless - Chrome even though the same interactions work on other screens. May - indicate a focus/z-index issue. Workaround: the test drives the dialog via - the Angular controller scope. -4. **Schema Designer races itself**: creating a schema and analyzing sample - docs transiently fails with `Failed to persist managed schema ... version - mismatch, retry` from its own `prep`/`analyze` calls, surfacing an error - dialog the user has to dismiss. Workaround: the test retries via the - offered Reload Schema button and ignores the designer's own 5xx console - errors. -5. **Plugins screen 500s when metrics are disabled**: `/admin/metrics` with - `wt=prometheus` returns HTTP 500 ("No metrics found in response") when - metrics collection is disabled, instead of a clean error; the Plugins - screen just shows nothing while the console logs the 500. Workaround: - tests enable `metricsEnabled`. -6. **Core overview ping widget logs a 503**: with a configset that has no - healthcheck file, the ping status call answers 503 and the console shows a - resource-load error on every visit; the widget could handle "healthcheck - not configured" gracefully. Workaround: allowed in the affected tests. -7. **Reload success indicator is a 1-second flash**: the Collections screen's - reload button only flags success via a CSS class for one second, which is - easy to miss (and impossible to assert on reliably). Workaround: the test - verifies the reload via the core start time instead. -8. **ui-grid icon font is missing from the webapp**: `css/angular/ui-grid.min.css` - references `fonts/ui-grid.woff` (and .ttf/.eot), but no such font files are - shipped anywhere under `solr/webapp/web` — the SQL screen's result grid - logs a 404 for it in production too, and grid icons render as boxes. - Workaround: the console-error assertion filters this 404. - ## Known limitations - The generated js-client bundle (`libs/solr/index.js`) only exists inside the From 3871a9942165aaebdf2bdb8d37f4d557737e386f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 14 Aug 2026 10:34:12 +0200 Subject: [PATCH 30/30] Change awaitsFix URL of AdminUiSchemaDesignerTest to point to SOLR-18347 --- .../org/apache/solr/webapp/AdminUiSchemaDesignerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java index daa0370ec043..319c1d459526 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java @@ -27,9 +27,9 @@ * *

AwaitsFix: the designer backend transiently fails its own prep/analyze calls ("version * mismatch, retry", "Error loading solr config") when driven at automation speed, making this test - * flaky even with retries; see the "Possible UI bugs" section in dev-docs/admin-ui-tests.md. + * flaky even with retries. */ -@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-8474") +@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-18347") public class AdminUiSchemaDesignerTest extends AdminUiTestBase { @Test