You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2022/06/03 17:57:06 UTC

[solr] branch main updated: SOLR-15743: Convert v2 GET /config APIs to annotations (#888)

This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new f4a71a9c45b SOLR-15743: Convert v2 GET /config APIs to annotations (#888)
f4a71a9c45b is described below

commit f4a71a9c45bbca3b4c5acf2c04de28f30653ecee
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Fri Jun 3 13:57:00 2022 -0400

    SOLR-15743: Convert v2 GET /config APIs to annotations (#888)
    
    Solr's been in the slow process of moving its v2 APIs away from the
    existing apispec/mapping framework towards one that relies on more
    explicit annotations to specify API properties.
    
    This commit converts the APIs from the core.config and core.config.Params
    apispec file to the "new" framework.  However, many other APIs under
    /config still remain.
    
    This commit also introduces a slight refactor in the testing for these
    sort of annotated APIs: a new test-base (V2ApiMappingTest) that collects
    test setup and assertion methods common to many of the v2-mapping test
    classes.
---
 .../org/apache/solr/handler/SolrConfigHandler.java |  15 +-
 .../solr/handler/admin/api/GetConfigAPI.java       |  73 +++++++
 .../solr/handler/admin/TestApiFramework.java       |   2 +-
 .../solr/handler/admin/V2ApiMappingTest.java       | 233 +++++++++++++++++++++
 .../admin/V2CollectionBackupsAPIMappingTest.java   |  77 ++-----
 .../handler/admin/V2CollectionsAPIMappingTest.java |  82 ++------
 .../solr/handler/admin/V2ConfigAPIMappingTest.java |  78 +++++++
 .../solr/handler/admin/V2CoresAPIMappingTest.java  | 125 +++--------
 .../admin/api/V2CollectionAPIMappingTest.java      | 103 ++-------
 .../handler/admin/api/V2CoreAPIMappingTest.java    | 111 ++++------
 .../handler/admin/api/V2ShardsAPIMappingTest.java  | 114 ++++------
 .../apispec/core.config.Commands.runtimeLib.json   |  23 --
 .../src/resources/apispec/core.config.Params.json  |  13 --
 solr/solrj/src/resources/apispec/core.config.json  |  18 --
 14 files changed, 539 insertions(+), 528 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index f66d13ca619..f86235f6263 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -52,6 +52,7 @@ import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
 import org.apache.solr.api.ApiBag;
 import org.apache.solr.client.solrj.SolrClient;
@@ -83,6 +84,7 @@ import org.apache.solr.core.RequestParams;
 import org.apache.solr.core.SolrConfig;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.handler.admin.api.GetConfigAPI;
 import org.apache.solr.pkg.PackageAPI;
 import org.apache.solr.pkg.PackageListeners;
 import org.apache.solr.request.LocalSolrQueryRequest;
@@ -317,6 +319,7 @@ public class SolrConfigHandler extends RequestHandlerBase
       boolean showParams = req.getParams().getBool("expandParams", false);
       Map<String, Object> map = this.req.getCore().getSolrConfig().toMap(new LinkedHashMap<>());
       if (componentType != null && !SolrRequestHandler.TYPE.equals(componentType)) return map;
+
       @SuppressWarnings({"unchecked"})
       Map<String, Object> reqHandlers =
           (Map<String, Object>)
@@ -1035,12 +1038,12 @@ public class SolrConfigHandler extends RequestHandlerBase
 
   @Override
   public Collection<Api> getApis() {
-    return ApiBag.wrapRequestHandlers(
-        this,
-        "core.config",
-        "core.config.Commands",
-        "core.config.Params",
-        "core.config.Params.Commands");
+    final List<Api> apis =
+        new ArrayList<>(
+            ApiBag.wrapRequestHandlers(
+                this, "core.config.Commands", "core.config.Params.Commands"));
+    apis.addAll(AnnotatedApi.getApis(new GetConfigAPI(this)));
+    return apis;
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.java
new file mode 100644
index 00000000000..8c8dcd25d53
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetConfigAPI.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.handler.admin.api;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
+
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.handler.SolrConfigHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 APIs for retrieving some or all configuration relevant to a particular collection (or core).
+ *
+ * <p>This class covers a handful of distinct paths under GET /v2/c/collectionName/config: /,
+ * /overlay, /params, /znodeVersion, etc.
+ */
+public class GetConfigAPI {
+  private final SolrConfigHandler configHandler;
+
+  public GetConfigAPI(SolrConfigHandler configHandler) {
+    this.configHandler = configHandler;
+  }
+
+  @EndPoint(
+      path = {"/config"},
+      method = GET,
+      permission = CONFIG_READ_PERM)
+  public void getAllConfig(SolrQueryRequest req, SolrQueryResponse rsp) {
+    configHandler.handleRequest(req, rsp);
+  }
+
+  @EndPoint(
+      path = {"/config/params"},
+      method = GET,
+      permission = CONFIG_READ_PERM)
+  public void getAllParamSets(SolrQueryRequest req, SolrQueryResponse rsp) {
+    configHandler.handleRequest(req, rsp);
+  }
+
+  @EndPoint(
+      path = {"/config/params/{paramset}"},
+      method = GET,
+      permission = CONFIG_READ_PERM)
+  public void getSingleParamSet(SolrQueryRequest req, SolrQueryResponse rsp) {
+    configHandler.handleRequest(req, rsp);
+  }
+
+  // This endpoint currently covers a whole list of paths by using the "component" placeholder
+  @EndPoint(
+      path = {"/config/{component}"},
+      method = GET,
+      permission = CONFIG_READ_PERM)
+  public void getComponentConfig(SolrQueryRequest req, SolrQueryResponse rsp) {
+    configHandler.handleRequest(req, rsp);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index c9c031fb34b..04be03acca4 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -167,7 +167,7 @@ public class TestApiFramework extends SolrTestCaseJ4 {
         rsp.getValues().asMap(2),
         Map.of(
             "/availableSubPaths", NOT_NULL,
-            "availableSubPaths /collections/hello/config/jmx", NOT_NULL,
+            "availableSubPaths /collections/hello/config/{component}", NOT_NULL,
             "availableSubPaths /collections/hello/schema", NOT_NULL,
             "availableSubPaths /collections/hello/shards", NOT_NULL,
             "availableSubPaths /collections/hello/shards/{shard}", NOT_NULL,
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2ApiMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2ApiMappingTest.java
new file mode 100644
index 00000000000..585742c2598
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2ApiMappingTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.handler.admin;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.ContentStreamBase;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+/**
+ * A base class that facilitates testing V2 to v1 API mappings by mocking out the underlying request
+ * handler and "capturing" the v1 request that the handler "sees" following v2-to-v1 conversion.
+ *
+ * <p>This base class is only appropriate for testing those v2 APIs implemented as a mapping-layer
+ * on top of an existing RequestHandler based implementation. API classes containing "real" logic or
+ * that don't rely on an underlying RequestHandler for their logic should use a different test
+ * harness.
+ *
+ * <p>Subclasses are required to implement {@link #populateApiBag()}, {@link #isCoreSpecific()}, and
+ * {@link #createUnderlyingRequestHandler()} to properly use this harness. See the method-level
+ * Javadocs on each for more information. With these methods implemented, subclasses can test their
+ * API mappings by calling any of the 'capture' methods included on this harness, which take in the
+ * v2 API path, method, etc. and use them to lookup and run the matching API (if a match can be
+ * found). Most of these helper methods return the (post-v1-conversion) {@link SolrParams} seen by
+ * the mocked RequestHandler.
+ */
+public abstract class V2ApiMappingTest<T extends RequestHandlerBase> extends SolrTestCaseJ4 {
+
+  protected ApiBag apiBag;
+  protected ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
+  @Mock protected T mockRequestHandler;
+
+  /**
+   * A hook allowing subclasses to insert the v2 endpoints-under-test into the {@link ApiBag}
+   * container.
+   */
+  public abstract void populateApiBag();
+
+  /**
+   * Instantiates a Mockito mock for the particular RequestHandler used by endpoints under test.
+   *
+   * <p>The created mock is used to capture the v1 request that actually makes its way to the
+   * request handler. Subclasses may stub out specific response values or behaviors as desired, but
+   * in most cases an unmodified mock (i.e. {@code return mock(CollectionsHandler.class)}) is
+   * sufficient.
+   */
+  public abstract T createUnderlyingRequestHandler();
+
+  /**
+   * Indicates whether the {@link ApiBag} used to lookup and invoke v2 APIs should be "core-level"
+   * (i.e. core- specific) or "container-level" (i.e. core-agnostic).
+   */
+  public abstract boolean isCoreSpecific();
+
+  public T getRequestHandler() {
+    return mockRequestHandler;
+  }
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void setUpMocks() {
+    mockRequestHandler = createUnderlyingRequestHandler();
+    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+
+    apiBag = new ApiBag(isCoreSpecific());
+    populateApiBag();
+  }
+
+  protected SolrQueryRequest captureConvertedV1Request(
+      String v2Path, String v2Method, String v2RequestBody) throws Exception {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(v2Path, v2Method, parts);
+    final SolrQueryResponse rsp = new SolrQueryResponse();
+    final LocalSolrQueryRequest req =
+        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
+          @Override
+          public List<CommandOperation> getCommands(boolean validateInput) {
+            if (v2RequestBody == null) return Collections.emptyList();
+            return ApiBag.getCommandOperations(
+                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
+          }
+
+          @Override
+          public Map<String, String> getPathTemplateValues() {
+            return parts;
+          }
+
+          @Override
+          public String getHttpMethod() {
+            return v2Method;
+          }
+        };
+
+    api.call(req, rsp);
+    verify(mockRequestHandler).handleRequestBody(queryRequestCaptor.capture(), any());
+    return queryRequestCaptor.getValue();
+  }
+
+  protected SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
+      throws Exception {
+    return captureConvertedV1Request(path, method, v2RequestBody).getParams();
+  }
+
+  protected SolrParams captureConvertedV1Params(
+      String path, String method, Map<String, String[]> queryParams) throws Exception {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, method, parts);
+    final SolrQueryResponse rsp = new SolrQueryResponse();
+    final LocalSolrQueryRequest req =
+        new LocalSolrQueryRequest(null, queryParams) {
+          @Override
+          public List<CommandOperation> getCommands(boolean validateInput) {
+            return Collections.emptyList();
+          }
+
+          @Override
+          public Map<String, String> getPathTemplateValues() {
+            return parts;
+          }
+
+          @Override
+          public String getHttpMethod() {
+            return method;
+          }
+        };
+
+    api.call(req, rsp);
+    verify(mockRequestHandler).handleRequestBody(queryRequestCaptor.capture(), any());
+    return queryRequestCaptor.getValue().getParams();
+  }
+
+  // TODO Combine with method above
+  protected SolrParams captureConvertedV1Params(String path, String method, SolrParams queryParams)
+      throws Exception {
+    return captureConvertedV1Params(path, method, queryParams, null);
+  }
+
+  protected SolrParams captureConvertedV1Params(
+      String path, String method, SolrParams queryParams, String v2RequestBody) throws Exception {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, method, parts);
+    final SolrQueryResponse rsp = new SolrQueryResponse();
+    final LocalSolrQueryRequest req =
+        new LocalSolrQueryRequest(null, queryParams) {
+          @Override
+          public List<CommandOperation> getCommands(boolean validateInput) {
+            if (v2RequestBody == null) return Collections.emptyList();
+            return ApiBag.getCommandOperations(
+                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
+          }
+
+          @Override
+          public Map<String, String> getPathTemplateValues() {
+            return parts;
+          }
+
+          @Override
+          public String getHttpMethod() {
+            return method;
+          }
+        };
+
+    api.call(req, rsp);
+    verify(mockRequestHandler).handleRequestBody(queryRequestCaptor.capture(), any());
+    return queryRequestCaptor.getValue().getParams();
+  }
+
+  protected void assertAnnotatedApiExistsForGET(String path) {
+    final AnnotatedApi api = getAnnotatedApiForGET(path);
+    assertTrue("Expected to find API mapping for path [" + path + "] but none found!", api != null);
+  }
+
+  protected AnnotatedApi getAnnotatedApiForGET(String path) {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, "GET", parts);
+    if (api == null) {
+      fail("Expected to find API for path [" + path + "], but no API mapping found.");
+    }
+    if (!(api instanceof AnnotatedApi)) {
+      fail(
+          "Expected AnnotatedApi for path ["
+              + path
+              + "], but found non-annotated API ["
+              + api
+              + "]");
+    }
+
+    return (AnnotatedApi) api;
+  }
+
+  protected T createMock(Class<T> clazz) {
+    return mock(clazz);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionBackupsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionBackupsAPIMappingTest.java
index 9f894682518..57cb85d95fb 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionBackupsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionBackupsAPIMappingTest.java
@@ -18,52 +18,29 @@ package org.apache.solr.handler.admin;
 
 import static org.apache.solr.common.params.CommonParams.ACTION;
 import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
-import com.google.common.collect.Maps;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CommonAdminParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.handler.CollectionBackupsAPI;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
-public class V2CollectionBackupsAPIMappingTest extends SolrTestCaseJ4 {
-
-  private ApiBag apiBag;
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CollectionsHandler mockCollectionsHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+public class V2CollectionBackupsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler> {
+  @Override
+  public void populateApiBag() {
+    final CollectionBackupsAPI collBackupsAPI = new CollectionBackupsAPI(getRequestHandler());
+    apiBag.registerObject(collBackupsAPI);
   }
 
-  @Before
-  public void setupApiBag() {
-    mockCollectionsHandler = mock(CollectionsHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+  @Override
+  public CollectionsHandler createUnderlyingRequestHandler() {
+    return createMock(CollectionsHandler.class);
+  }
 
-    apiBag = new ApiBag(false);
-    final CollectionBackupsAPI collBackupsAPI = new CollectionBackupsAPI(mockCollectionsHandler);
-    apiBag.registerObject(collBackupsAPI);
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
   @Test
@@ -110,34 +87,4 @@ public class V2CollectionBackupsAPIMappingTest extends SolrTestCaseJ4 {
     assertEquals("/some/location/uri", v1Params.get(CoreAdminParams.BACKUP_LOCATION));
     assertEquals("someRepository", v1Params.get(CoreAdminParams.BACKUP_REPOSITORY));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
index 788e7494974..dc52e1bb2ae 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
@@ -17,19 +17,8 @@
 package org.apache.solr.handler.admin;
 
 import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
-import com.google.common.collect.Maps;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.cloud.api.collections.CategoryRoutedAlias;
 import org.apache.solr.cloud.api.collections.RoutedAlias;
@@ -42,17 +31,9 @@ import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.core.backup.BackupManager;
 import org.apache.solr.handler.CollectionsAPI;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /**
  * Unit tests for the API mappings found in {@link org.apache.solr.handler.CollectionsAPI}.
@@ -68,27 +49,23 @@ import org.mockito.ArgumentCaptor;
  * provided. This is done to exercise conversion of all parameters, even if particular combinations
  * are never expected in the same request.
  */
-public class V2CollectionsAPIMappingTest extends SolrTestCaseJ4 {
+public class V2CollectionsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler> {
 
-  private ApiBag apiBag;
-
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CollectionsHandler mockCollectionsHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+  @Override
+  public void populateApiBag() {
+    final CollectionsAPI collectionsAPI = new CollectionsAPI(getRequestHandler());
+    apiBag.registerObject(collectionsAPI);
+    apiBag.registerObject(collectionsAPI.collectionsCommands);
   }
 
-  @Before
-  public void setupApiBag() {
-    mockCollectionsHandler = mock(CollectionsHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+  @Override
+  public CollectionsHandler createUnderlyingRequestHandler() {
+    return createMock(CollectionsHandler.class);
+  }
 
-    apiBag = new ApiBag(false);
-    final CollectionsAPI collectionsAPI = new CollectionsAPI(mockCollectionsHandler);
-    apiBag.registerObject(collectionsAPI);
-    apiBag.registerObject(collectionsAPI.collectionsCommands);
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
   @Test
@@ -140,7 +117,8 @@ public class V2CollectionsAPIMappingTest extends SolrTestCaseJ4 {
 
   @Test
   public void testListCollectionsAllProperties() throws Exception {
-    final SolrParams v1Params = captureConvertedV1Params("/collections", "GET", null);
+    final String noBody = null;
+    final SolrParams v1Params = captureConvertedV1Params("/collections", "GET", noBody);
 
     assertEquals(CollectionParams.CollectionAction.LIST.lowerName, v1Params.get(ACTION));
   }
@@ -303,34 +281,4 @@ public class V2CollectionsAPIMappingTest extends SolrTestCaseJ4 {
     assertEquals("bar2", v1Params.get("property.foo2"));
     assertEquals(3, v1Params.getPrimitiveInt(ZkStateReader.REPLICATION_FACTOR));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
new file mode 100644
index 00000000000..8d55748a5ee
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2ConfigAPIMappingTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.handler.admin;
+
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.handler.SolrConfigHandler;
+import org.apache.solr.handler.admin.api.GetConfigAPI;
+import org.junit.Test;
+
+/** Unit tests for the GET /v2/c/collectionName/config APIs. */
+public class V2ConfigAPIMappingTest extends V2ApiMappingTest<SolrConfigHandler> {
+
+  @Override
+  public SolrConfigHandler createUnderlyingRequestHandler() {
+    return createMock(SolrConfigHandler.class);
+  }
+
+  @Override
+  public boolean isCoreSpecific() {
+    return true;
+  }
+
+  @Override
+  public void populateApiBag() {
+    apiBag.registerObject(new GetConfigAPI(getRequestHandler()));
+  }
+
+  // GET /v2/c/collectionName/config is a pure pass-through to the underlying request handler
+  @Test
+  public void testGetAllConfig() throws Exception {
+    assertAnnotatedApiExistsForGET("/config");
+  }
+
+  // GET /v2/collectionName/config/<component> is a pure pass-through to the underlying request
+  // handler.  Just check
+  // the API lookup works for a handful of the valid config "components".
+  @Test
+  public void testGetSingleComponentConfig() throws Exception {
+    assertAnnotatedApiExistsForGET("/config/overlay");
+    assertAnnotatedApiExistsForGET("/config/query");
+    assertAnnotatedApiExistsForGET("/config/jmx");
+    assertAnnotatedApiExistsForGET("/config/requestDispatcher");
+    assertAnnotatedApiExistsForGET("/config/znodeVersion");
+    assertAnnotatedApiExistsForGET("/config/luceneMatchVersion");
+  }
+
+  @Test
+  public void testGetParamsetsConfig() throws Exception {
+    assertAnnotatedApiExistsForGET("/config/params");
+    final AnnotatedApi getParamSetsApi = getAnnotatedApiForGET("/config/params");
+    // Ensure that the /config/params path redirects to the /config/params specific endpoint (and
+    // not the generic "/config/{component}")
+    final String[] getParamSetsApiPaths = getParamSetsApi.getEndPoint().path();
+    assertEquals(1, getParamSetsApiPaths.length);
+    assertEquals("/config/params", getParamSetsApiPaths[0]);
+
+    assertAnnotatedApiExistsForGET("/config/params/someParamSet");
+    final AnnotatedApi getSingleParamSetApi = getAnnotatedApiForGET("/config/params/someParamSet");
+    final String[] getSingleParamsetApiPaths = getSingleParamSetApi.getEndPoint().path();
+    assertEquals(1, getSingleParamsetApiPaths.length);
+    assertEquals("/config/params/{paramset}", getSingleParamsetApiPaths[0]);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2CoresAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2CoresAPIMappingTest.java
index 92a305cb86c..fe847b20d89 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2CoresAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2CoresAPIMappingTest.java
@@ -20,35 +20,33 @@ package org.apache.solr.handler.admin;
 import static org.apache.solr.common.params.CollectionAdminParams.NUM_SHARDS;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CoreAdminParams.*;
+import static org.apache.solr.common.params.CoreAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CoreAdminParams.CONFIG;
+import static org.apache.solr.common.params.CoreAdminParams.CONFIGSET;
+import static org.apache.solr.common.params.CoreAdminParams.CORE;
+import static org.apache.solr.common.params.CoreAdminParams.CORE_NODE_NAME;
 import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.CREATE;
 import static org.apache.solr.common.params.CoreAdminParams.CoreAdminAction.STATUS;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
+import static org.apache.solr.common.params.CoreAdminParams.DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.INDEX_INFO;
+import static org.apache.solr.common.params.CoreAdminParams.INSTANCE_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.LOAD_ON_STARTUP;
+import static org.apache.solr.common.params.CoreAdminParams.NAME;
+import static org.apache.solr.common.params.CoreAdminParams.NEW_COLLECTION;
+import static org.apache.solr.common.params.CoreAdminParams.REPLICA_TYPE;
+import static org.apache.solr.common.params.CoreAdminParams.ROLES;
+import static org.apache.solr.common.params.CoreAdminParams.SCHEMA;
+import static org.apache.solr.common.params.CoreAdminParams.SHARD;
+import static org.apache.solr.common.params.CoreAdminParams.TRANSIENT;
+import static org.apache.solr.common.params.CoreAdminParams.ULOG_DIR;
 
-import com.google.common.collect.Maps;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.handler.admin.api.AllCoresStatusAPI;
 import org.apache.solr.handler.admin.api.CreateCoreAPI;
 import org.apache.solr.handler.admin.api.SingleCoreStatusAPI;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /**
  * Unit tests for the /cores APIs.
@@ -58,25 +56,24 @@ import org.mockito.ArgumentCaptor;
  * provided. This is done to exercise conversion of all parameters, even if particular combinations
  * are never expected in the same request.
  */
-public class V2CoresAPIMappingTest extends SolrTestCaseJ4 {
-  private ApiBag apiBag;
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CoreAdminHandler mockCoreAdminHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+public class V2CoresAPIMappingTest extends V2ApiMappingTest<CoreAdminHandler> {
+
+  @Override
+  public void populateApiBag() {
+    final CoreAdminHandler handler = getRequestHandler();
+    apiBag.registerObject(new CreateCoreAPI(handler));
+    apiBag.registerObject(new SingleCoreStatusAPI(handler));
+    apiBag.registerObject(new AllCoresStatusAPI(handler));
   }
 
-  @Before
-  public void setUpMocks() {
-    mockCoreAdminHandler = mock(CoreAdminHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+  @Override
+  public CoreAdminHandler createUnderlyingRequestHandler() {
+    return createMock(CoreAdminHandler.class);
+  }
 
-    apiBag = new ApiBag(false);
-    apiBag.registerObject(new CreateCoreAPI(mockCoreAdminHandler));
-    apiBag.registerObject(new SingleCoreStatusAPI(mockCoreAdminHandler));
-    apiBag.registerObject(new AllCoresStatusAPI(mockCoreAdminHandler));
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
   @Test
@@ -148,62 +145,4 @@ public class V2CoresAPIMappingTest extends SolrTestCaseJ4 {
     assertNull("Expected 'core' parameter to be null", v1Params.get(CORE));
     assertEquals(true, v1Params.getPrimitiveBool(INDEX_INFO));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCoreAdminHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
-
-  private SolrParams captureConvertedV1Params(
-      String path, String method, Map<String, String[]> queryParams) throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, queryParams) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            return Collections.emptyList();
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCoreAdminHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
index bf2af4e6f31..7724f530588 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
@@ -23,33 +23,16 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.ACTION;
 import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.common.params.CoreAdminParams.SHARD;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
-import com.google.common.collect.Maps;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.handler.admin.TestCollectionAPIs;
+import org.apache.solr.handler.admin.V2ApiMappingTest;
 import org.apache.solr.handler.api.ApiRegistrar;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /**
  * Unit tests for the V2 APIs found in {@link org.apache.solr.handler.admin.api} that use the
@@ -66,24 +49,20 @@ import org.mockito.ArgumentCaptor;
  * provided. This is done to exercise conversion of all parameters, even if particular combinations
  * are never expected in the same request.
  */
-public class V2CollectionAPIMappingTest extends SolrTestCaseJ4 {
-  private ApiBag apiBag;
-
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CollectionsHandler mockCollectionsHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+public class V2CollectionAPIMappingTest extends V2ApiMappingTest<CollectionsHandler> {
+  @Override
+  public void populateApiBag() {
+    ApiRegistrar.registerCollectionApis(apiBag, getRequestHandler());
   }
 
-  @Before
-  public void setupApiBag() {
-    mockCollectionsHandler = mock(CollectionsHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+  @Override
+  public CollectionsHandler createUnderlyingRequestHandler() {
+    return createMock(CollectionsHandler.class);
+  }
 
-    apiBag = new ApiBag(false);
-    ApiRegistrar.registerCollectionApis(apiBag, mockCollectionsHandler);
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
   @Test
@@ -280,62 +259,4 @@ public class V2CollectionAPIMappingTest extends SolrTestCaseJ4 {
     assertEquals("somePropertyName", v1Params.get("propertyName"));
     assertEquals("somePropertyValue", v1Params.get("propertyValue"));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
-
-  private SolrParams captureConvertedV1Params(
-      String path, String method, Map<String, String[]> queryParams) throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, queryParams) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            return Collections.emptyList();
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CoreAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CoreAPIMappingTest.java
index a11dfce9750..fc5ffb51a9e 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CoreAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CoreAPIMappingTest.java
@@ -21,33 +21,25 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonAdminParams.SPLIT_KEY;
 import static org.apache.solr.common.params.CommonParams.ACTION;
 import static org.apache.solr.common.params.CommonParams.PATH;
-import static org.apache.solr.common.params.CoreAdminParams.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
+import static org.apache.solr.common.params.CoreAdminParams.CORE;
+import static org.apache.solr.common.params.CoreAdminParams.CORE_NODE_NAME;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.NAME;
+import static org.apache.solr.common.params.CoreAdminParams.OTHER;
+import static org.apache.solr.common.params.CoreAdminParams.RANGES;
+import static org.apache.solr.common.params.CoreAdminParams.REQUESTID;
+import static org.apache.solr.common.params.CoreAdminParams.TARGET_CORE;
 
-import com.google.common.collect.Maps;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.params.UpdateParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.handler.admin.CoreAdminHandler;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
+import org.apache.solr.handler.admin.V2ApiMappingTest;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /**
  * Unit tests for the V2 APIs found in {@link org.apache.solr.handler.admin.api} that use the
@@ -58,36 +50,35 @@ import org.mockito.ArgumentCaptor;
  * provided. This is done to exercise conversion of all parameters, even if particular combinations
  * are never expected in the same request.
  */
-public class V2CoreAPIMappingTest extends SolrTestCaseJ4 {
-  private ApiBag apiBag;
+public class V2CoreAPIMappingTest extends V2ApiMappingTest<CoreAdminHandler> {
 
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
+  private static final String NO_BODY = null;
 
-  private CoreAdminHandler mockCoreHandler;
+  @Override
+  public CoreAdminHandler createUnderlyingRequestHandler() {
+    return createMock(CoreAdminHandler.class);
+  }
 
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
-  @Before
-  public void setupApiBag() {
-    mockCoreHandler = mock(CoreAdminHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
-
-    apiBag = new ApiBag(false);
-    apiBag.registerObject(new ReloadCoreAPI(mockCoreHandler));
-    apiBag.registerObject(new SwapCoresAPI(mockCoreHandler));
-    apiBag.registerObject(new RenameCoreAPI(mockCoreHandler));
-    apiBag.registerObject(new UnloadCoreAPI(mockCoreHandler));
-    apiBag.registerObject(new MergeIndexesAPI(mockCoreHandler));
-    apiBag.registerObject(new SplitCoreAPI(mockCoreHandler));
-    apiBag.registerObject(new RequestCoreRecoveryAPI(mockCoreHandler));
-    apiBag.registerObject(new PrepareCoreRecoveryAPI(mockCoreHandler));
-    apiBag.registerObject(new RequestApplyCoreUpdatesAPI(mockCoreHandler));
-    apiBag.registerObject(new RequestSyncShardAPI(mockCoreHandler));
-    apiBag.registerObject(new RequestBufferUpdatesAPI(mockCoreHandler));
-    apiBag.registerObject(new RequestCoreCommandStatusAPI(mockCoreHandler));
+  @Override
+  public void populateApiBag() {
+    final CoreAdminHandler handler = getRequestHandler();
+    apiBag.registerObject(new ReloadCoreAPI(handler));
+    apiBag.registerObject(new SwapCoresAPI(handler));
+    apiBag.registerObject(new RenameCoreAPI(handler));
+    apiBag.registerObject(new UnloadCoreAPI(handler));
+    apiBag.registerObject(new MergeIndexesAPI(handler));
+    apiBag.registerObject(new SplitCoreAPI(handler));
+    apiBag.registerObject(new RequestCoreRecoveryAPI(handler));
+    apiBag.registerObject(new PrepareCoreRecoveryAPI(handler));
+    apiBag.registerObject(new RequestApplyCoreUpdatesAPI(handler));
+    apiBag.registerObject(new RequestSyncShardAPI(handler));
+    apiBag.registerObject(new RequestBufferUpdatesAPI(handler));
+    apiBag.registerObject(new RequestCoreCommandStatusAPI(handler));
   }
 
   @Test
@@ -261,39 +252,9 @@ public class V2CoreAPIMappingTest extends SolrTestCaseJ4 {
   @Test
   public void testRequestCommandStatusAllParams() throws Exception {
     final SolrParams v1Params =
-        captureConvertedV1Params("/cores/coreName/command-status/someId", "GET", null);
+        captureConvertedV1Params("/cores/coreName/command-status/someId", "GET", NO_BODY);
 
     assertEquals("requeststatus", v1Params.get(ACTION));
     assertEquals("someId", v1Params.get(REQUESTID));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, Maps.newHashMap()) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCoreHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
index 127183c386c..36140d307a4 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
@@ -21,36 +21,41 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
-import static org.apache.solr.common.cloud.ZkStateReader.*;
-import static org.apache.solr.common.params.CollectionAdminParams.*;
+import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.COUNT_PROP;
+import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionParams.NAME;
-import static org.apache.solr.common.params.CommonAdminParams.*;
-import static org.apache.solr.common.params.CommonParams.*;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.NUM_SUB_SHARDS;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_BY_PREFIX;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_FUZZ;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_KEY;
+import static org.apache.solr.common.params.CommonAdminParams.SPLIT_METHOD;
+import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
 import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.common.params.CoreAdminParams.*;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
+import static org.apache.solr.common.params.CommonParams.TIMING;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_DATA_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INDEX;
+import static org.apache.solr.common.params.CoreAdminParams.DELETE_INSTANCE_DIR;
+import static org.apache.solr.common.params.CoreAdminParams.REPLICA;
+import static org.apache.solr.common.params.CoreAdminParams.SHARD;
 
-import java.util.*;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
-import org.apache.solr.common.params.*;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStreamBase;
+import java.util.Locale;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.handler.admin.V2ApiMappingTest;
 import org.apache.solr.handler.api.ApiRegistrar;
-import org.apache.solr.request.LocalSolrQueryRequest;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 
 /**
  * Unit tests for the V2 APIs that use the /c/{collection}/shards or /c/{collection}/shards/{shard}
@@ -61,24 +66,21 @@ import org.mockito.ArgumentCaptor;
  * provided. This is done to exercise conversion of all parameters, even if particular combinations
  * are never expected in the same request.
  */
-public class V2ShardsAPIMappingTest {
-  private ApiBag apiBag;
+public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler> {
 
-  private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
-  private CollectionsHandler mockCollectionsHandler;
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
+  @Override
+  public void populateApiBag() {
+    ApiRegistrar.registerShardApis(apiBag, getRequestHandler());
   }
 
-  @Before
-  public void setupApiBag() {
-    mockCollectionsHandler = mock(CollectionsHandler.class);
-    queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
+  @Override
+  public CollectionsHandler createUnderlyingRequestHandler() {
+    return createMock(CollectionsHandler.class);
+  }
 
-    apiBag = new ApiBag(false);
-    ApiRegistrar.registerShardApis(apiBag, mockCollectionsHandler);
+  @Override
+  public boolean isCoreSpecific() {
+    return false;
   }
 
   @Test
@@ -270,44 +272,4 @@ public class V2ShardsAPIMappingTest {
     assertEquals("4", v1Params.get(COUNT_PROP));
     assertEquals("true", v1Params.get(ONLY_IF_DOWN));
   }
-
-  private SolrParams captureConvertedV1Params(String path, String method, SolrParams queryParams)
-      throws Exception {
-    return captureConvertedV1Params(path, method, queryParams, null);
-  }
-
-  private SolrParams captureConvertedV1Params(String path, String method, String v2RequestBody)
-      throws Exception {
-    return captureConvertedV1Params(path, method, new ModifiableSolrParams(), v2RequestBody);
-  }
-
-  private SolrParams captureConvertedV1Params(
-      String path, String method, SolrParams queryParams, String v2RequestBody) throws Exception {
-    final HashMap<String, String> parts = new HashMap<>();
-    final Api api = apiBag.lookup(path, method, parts);
-    final SolrQueryResponse rsp = new SolrQueryResponse();
-    final LocalSolrQueryRequest req =
-        new LocalSolrQueryRequest(null, queryParams) {
-          @Override
-          public List<CommandOperation> getCommands(boolean validateInput) {
-            if (v2RequestBody == null) return Collections.emptyList();
-            return ApiBag.getCommandOperations(
-                new ContentStreamBase.StringStream(v2RequestBody), api.getCommandSchema(), true);
-          }
-
-          @Override
-          public Map<String, String> getPathTemplateValues() {
-            return parts;
-          }
-
-          @Override
-          public String getHttpMethod() {
-            return method;
-          }
-        };
-
-    api.call(req, rsp);
-    verify(mockCollectionsHandler).handleRequestBody(queryRequestCaptor.capture(), any());
-    return queryRequestCaptor.getValue().getParams();
-  }
 }
diff --git a/solr/solrj/src/resources/apispec/core.config.Commands.runtimeLib.json b/solr/solrj/src/resources/apispec/core.config.Commands.runtimeLib.json
deleted file mode 100644
index 89ec43aa92c..00000000000
--- a/solr/solrj/src/resources/apispec/core.config.Commands.runtimeLib.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/adding-custom-plugins-in-solrcloud-mode.html",
-  "description": "Allows you to register .jars that have been uploaded to the .system collection in Solr. Note that uploading the .jar must occur before using this API.",
-  "type": "object",
-  "properties": {
-    "name": {
-      "description": "The name of the .jar blob in .system collection. This is the name you provided when you uploaded it.",
-      "type": "string"
-    },
-    "version": {
-      "type": "integer",
-      "description": "The version of the blob in .system collection. Be sure to use the correct version if you have multiple versions of the same .jar uploaded."
-    },
-    "sig": {
-      "type": "string",
-      "description": "The sha1 signature of the .jar, if it was signed before uploading. If you signed the sha1 digest of your .jar file prior to uploading it to the .system collection, this is where you need to provide the signature."
-    }
-  },
-  "required": [
-    "name",
-    "version"
-  ]
-}
diff --git a/solr/solrj/src/resources/apispec/core.config.Params.json b/solr/solrj/src/resources/apispec/core.config.Params.json
deleted file mode 100644
index bce9af4e8f5..00000000000
--- a/solr/solrj/src/resources/apispec/core.config.Params.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/request-parameters-api.html",
-  "description": "List all parameter sets (paramsets). Individual paramsets can be requested by paramset name.",
-  "methods": [
-    "GET"
-  ],
-  "url": {
-    "paths": [
-      "/config/params",
-      "/config/params/{params_set}"
-    ]
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/core.config.json b/solr/solrj/src/resources/apispec/core.config.json
deleted file mode 100644
index b46b96b2d4e..00000000000
--- a/solr/solrj/src/resources/apispec/core.config.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/config-api.html",
-  "description": "Gets the Solr configuration for a collection.",
-  "methods": [
-    "GET"
-  ],
-  "url": {
-    "paths": [
-      "/config",
-      "/config/overlay",
-      "/config/query",
-      "/config/jmx",
-      "/config/requestDispatcher",
-      "/config/znodeVersion",
-      "/config/{plugin}"
-    ]
-  }
-}