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

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


The following commit(s) were added to refs/heads/main by this push:
     new 77e96886 feat(collection): add alias management tools (list, create, 
delete) (#214)
77e96886 is described below

commit 77e968867fe5339f6cc0d2e22aef9a330d43083c
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Oct 6 14:24:24 2026 -0400

    feat(collection): add alias management tools (list, create, delete) (#214)
    
    * feat: added alias management
    
    * Reverted locally failing test.
    
    * Usage of CollectionAdminResponse.getAliases().
    create-alias destructive and idempotent.
    Added tests.
    Updated documentation.
    
    * Added Alias integration test.
    
    * fix(alias): address PR review feedback
    
    - Derive success from response.getStatus() instead of hardcoding true
    - Add @JsonFormat to AliasResult timestamp for ISO 8601 consistency
    - Use separate TEST_COLLECTION_2 in integration test update scenario
    
    * style(alias): apply spotlessApply formatting
    
    The Build job runs './gradlew classes testClasses spotlessCheck' as a
    fail-fast gate, and spotlessJavaCheck rejected the new alias files for
    indentation (spaces where the project's format expects tabs). Because
    Unit Tests and Integration Tests are gated behind Build, this single
    formatting miss blocked the entire pipeline.
    
    No functional change - output of './gradlew spotlessApply'.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * fix(test): make createAlias_multipleAliasesCanExist order-independent
    
    createAlias_multipleAliasesCanExist (@Order(5)) asserted that TEST_ALIAS
    still pointed at TEST_COLLECTION, but createAlias_updatesExistingAlias
    (@Order(4)) had already repointed it to TEST_COLLECTION_2 - and asserts
    that it did. The expectation therefore contradicted the preceding test
    and failed deterministically:
    
        AliasServiceIntegrationTest > createAlias_multipleAliasesCanExist()
        FAILED java.lang.AssertionError at AliasServiceIntegrationTest.java:119
    
    AliasService itself is correct; only the test expectation was stale.
    
    Rather than just correcting the expected value, this makes the test
    assert solely on aliases it creates itself, so it no longer depends on
    what an earlier test left TEST_ALIAS pointing at. TEST_ALIAS_2 is still
    left in place for the deletion tests that follow; TEST_ALIAS_3 is this
    test's own fixture and is cleaned up in the test.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    ---------
    
    Signed-off-by: adityamparikh <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-authored-by: Luis Rojo <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
    Co-authored-by: Y. Luis <[email protected]>
---
 README.md                                          |   3 +
 .../solr/mcp/server/collection/AliasResult.java    |  43 +++++
 .../solr/mcp/server/collection/AliasService.java   | 215 +++++++++++++++++++++
 .../solr/mcp/server/config/SolrNativeHints.java    |   2 +-
 .../collection/AliasServiceIntegrationTest.java    | 156 +++++++++++++++
 .../mcp/server/collection/AliasServiceTest.java    | 173 +++++++++++++++++
 6 files changed, 591 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 54069bdd..4c96bb35 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,9 @@ Using a different client, or want STDIO/HTTP/Docker 
options? See the per-client
 | `add-fields` | Add fields to a collection schema (additive only; existing 
fields cannot be modified) |
 | `add-field-types` | Add field types — custom analyzers, `DenseVectorField` 
for semantic search, etc. |
 | `get-schema` | Retrieve schema information for a collection |
+| `list-aliases` | List all Solr aliases and the collections they point to |
+| `create-alias` | Create or update a Solr alias pointing to one or more 
collections |
+| `delete-alias` | Delete a Solr alias (underlying collections are not 
affected) |
 
 Every tool advertises MCP behavior hints (`readOnlyHint`, `destructiveHint`, 
`idempotentHint`) so clients can build sensible approval UX — `search` and the 
metadata tools are read-only, indexing is destructive but idempotent, schema 
modification is additive.
 
diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java 
b/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java
new file mode 100644
index 00000000..34f7561b
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import java.util.Date;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Result record for alias management operations.
+ *
+ * <p>
+ * Returned by {@link AliasService} methods to communicate the outcome of
+ * create, update, and delete operations on Solr aliases.
+ *
+ * @param aliasName
+ *            the alias that was operated on
+ * @param collections
+ *            the target collection(s) (null for delete operations)
+ * @param success
+ *            whether the operation completed successfully
+ * @param message
+ *            human-readable description of the outcome
+ * @param timestamp
+ *            when the operation was performed
+ */
+public record AliasResult(String aliasName, @Nullable String collections, 
boolean success, String message,
+               @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = 
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") Date timestamp) {
+}
diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java 
b/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java
new file mode 100644
index 00000000..bf024844
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import io.micrometer.observation.annotation.Observed;
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.springaicommunity.mcp.annotation.McpTool;
+import org.springaicommunity.mcp.annotation.McpToolParam;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+
+/**
+ * Spring Service providing Solr alias management capabilities for MCP clients.
+ *
+ * <p>
+ * Aliases are virtual collection names that point to one or more physical
+ * collections. They enable zero-downtime reindexing, blue-green deployments,
+ * and read/write separation by allowing applications to reference a stable 
name
+ * while the underlying collection is swapped transparently.
+ *
+ * <p>
+ * <strong>Core Capabilities:</strong>
+ *
+ * <ul>
+ * <li><strong>List Aliases</strong>: Discover all aliases and their target
+ * collections
+ * <li><strong>Create/Update Alias</strong>: Point an alias to one or more
+ * collections (creates if new, updates if existing)
+ * <li><strong>Delete Alias</strong>: Remove an alias without affecting the
+ * underlying collections
+ * </ul>
+ *
+ * @see CollectionAdminRequest
+ */
+@Service
+@Observed
+public class AliasService {
+
+       /**
+        * Error message for blank alias name validation
+        */
+       private static final String BLANK_ALIAS_NAME_ERROR = "Alias name must 
not be blank";
+
+       /**
+        * Error message for blank collections validation
+        */
+       private static final String BLANK_COLLECTIONS_ERROR = "Collections must 
not be blank";
+
+       /**
+        * SolrJ client for communicating with Solr server
+        */
+       private final SolrClient solrClient;
+
+       /**
+        * Constructs a new AliasService with the required dependencies.
+        *
+        * @param solrClient
+        *            the SolrJ client instance for communicating with Solr
+        */
+       public AliasService(SolrClient solrClient) {
+               this.solrClient = solrClient;
+       }
+
+       /**
+        * Lists all aliases defined in the Solr cluster.
+        *
+        * <p>
+        * Returns a map where each key is an alias name and the corresponding 
value is
+        * the comma-separated list of collection names that the alias points 
to.
+        *
+        * <p>
+        * <strong>MCP Tool Usage:</strong>
+        *
+        * <p>
+        * Invoked by AI clients with natural language requests like "list all 
aliases",
+        * "what aliases exist?", or "show me alias mappings".
+        *
+        * @return a map of alias names to their target collection(s); never 
null
+        *         (returns an empty map when no aliases are defined)
+        * @throws SolrServerException
+        *             if there are errors communicating with Solr
+        * @throws IOException
+        *             if there are I/O errors during communication
+        */
+       @PreAuthorize("isAuthenticated()")
+       @McpTool(
+                       name = "list-aliases",
+                       annotations = @McpTool.McpAnnotations(readOnlyHint = 
true),
+                       description = "List all Solr aliases and the 
collections they point to")
+       public Map<String, String> listAliases() throws SolrServerException, 
IOException {
+               CollectionAdminRequest.ListAliases request = new 
CollectionAdminRequest.ListAliases();
+               CollectionAdminResponse response = request.process(solrClient);
+               Map<String, String> aliases = response.getAliases();
+               return aliases != null ? aliases : Map.of();
+       }
+
+       /**
+        * Creates or updates a Solr alias pointing to one or more collections.
+        *
+        * <p>
+        * If the alias already exists, it is updated to point to the new 
collection(s).
+        * This is the mechanism for zero-downtime collection swaps: reindex 
into a new
+        * collection, then update the alias to point to it.
+        *
+        * <p>
+        * <strong>MCP Tool Usage:</strong>
+        *
+        * <p>
+        * Invoked with requests like "create an alias ORDERS pointing to 
ORDERS_V2" or
+        * "swap the LIVE alias to PRODUCTS_V3".
+        *
+        * @param aliasName
+        *            the name of the alias to create or update (must not be 
blank)
+        * @param collections
+        *            comma-separated list of target collection names (must not 
be
+        *            blank)
+        * @return result describing the outcome of the operation
+        * @throws IllegalArgumentException
+        *             if aliasName or collections is blank
+        * @throws SolrServerException
+        *             if Solr returns an error
+        * @throws IOException
+        *             if there are I/O errors during communication
+        */
+       @PreAuthorize("isAuthenticated()")
+       @McpTool(
+                       name = "create-alias",
+                       annotations = @McpTool.McpAnnotations(idempotentHint = 
true),
+                       description = "Create or update a Solr alias pointing 
to one or more collections. "
+                                       + "If the alias already exists, it is 
updated to point to the new collection(s).")
+       public AliasResult createAlias(
+                       @McpToolParam(description = "Name of the alias to 
create or update") String aliasName,
+                       @McpToolParam(
+                                       description = "Comma-separated list of 
collection names the alias should point to") String collections)
+                       throws SolrServerException, IOException {
+
+               if (aliasName == null || aliasName.isBlank()) {
+                       throw new 
IllegalArgumentException(BLANK_ALIAS_NAME_ERROR);
+               }
+               if (collections == null || collections.isBlank()) {
+                       throw new 
IllegalArgumentException(BLANK_COLLECTIONS_ERROR);
+               }
+
+               CollectionAdminRequest.CreateAlias request = 
CollectionAdminRequest.createAlias(aliasName, collections);
+               CollectionAdminResponse response = request.process(solrClient);
+               boolean success = response.getStatus() == 0;
+
+               return new AliasResult(aliasName, collections, success,
+                               success ? "Alias created/updated successfully" 
: "Alias creation/update failed", new Date());
+       }
+
+       /**
+        * Deletes an existing Solr alias.
+        *
+        * <p>
+        * Removes the alias definition only — the underlying collection(s) are 
not
+        * affected. After deletion, the alias name is no longer resolvable.
+        *
+        * <p>
+        * <strong>MCP Tool Usage:</strong>
+        *
+        * <p>
+        * Invoked with requests like "delete the stale alias ORDERS_TEST" or 
"remove
+        * alias OLD_PRODUCTS".
+        *
+        * @param aliasName
+        *            the name of the alias to delete (must not be blank)
+        * @return result describing the outcome of the operation
+        * @throws IllegalArgumentException
+        *             if aliasName is blank
+        * @throws SolrServerException
+        *             if Solr returns an error
+        * @throws IOException
+        *             if there are I/O errors during communication
+        */
+       @PreAuthorize("isAuthenticated()")
+       @McpTool(
+                       name = "delete-alias",
+                       annotations = @McpTool.McpAnnotations(destructiveHint = 
true),
+                       description = "Delete a Solr alias. The underlying 
collection(s) are not affected.")
+       public AliasResult deleteAlias(@McpToolParam(description = "Name of the 
alias to delete") String aliasName)
+                       throws SolrServerException, IOException {
+
+               if (aliasName == null || aliasName.isBlank()) {
+                       throw new 
IllegalArgumentException(BLANK_ALIAS_NAME_ERROR);
+               }
+
+               CollectionAdminRequest.DeleteAlias request = 
CollectionAdminRequest.deleteAlias(aliasName);
+               CollectionAdminResponse response = request.process(solrClient);
+               boolean success = response.getStatus() == 0;
+
+               return new AliasResult(aliasName, null, success,
+                               success ? "Alias deleted successfully" : "Alias 
deletion failed", new Date());
+       }
+}
diff --git 
a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java 
b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
index aecf59cf..d0ac3147 100644
--- a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
+++ b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
@@ -65,7 +65,7 @@ public class SolrNativeHints {
         * parameters in native image. Registered by name because some are
         * package-private.
         */
-       private static final List<String> MCP_TOOL_RECORDS = List.of(
+       private static final List<String> MCP_TOOL_RECORDS = 
List.of("org.apache.solr.mcp.server.collection.AliasResult",
                        
"org.apache.solr.mcp.server.collection.CollectionCreationResult",
                        
"org.apache.solr.mcp.server.collection.SolrHealthStatus",
                        "org.apache.solr.mcp.server.collection.SolrMetrics", 
"org.apache.solr.mcp.server.collection.IndexStats",
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java
new file mode 100644
index 00000000..7d7ba8fd
--- /dev/null
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.apache.solr.mcp.server.TestcontainersConfiguration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+/**
+ * Integration tests for {@link AliasService} using Testcontainers with a real
+ * Solr instance.
+ *
+ * <p>
+ * Tests create, list, update, and delete alias operations against a live Solr
+ * cluster.
+ */
+@SpringBootTest
+@Import(TestcontainersConfiguration.class)
+@Tag("integration")
+@Testcontainers(disabledWithoutDocker = true)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class AliasServiceIntegrationTest {
+
+       private static final String TEST_COLLECTION = "alias_test_collection";
+       private static final String TEST_COLLECTION_2 = 
"alias_test_collection_2";
+       private static final String TEST_ALIAS = "alias_test_alias";
+       private static final String TEST_ALIAS_2 = "alias_test_alias_2";
+       private static final String TEST_ALIAS_3 = "alias_test_alias_3";
+
+       @Autowired
+       private AliasService aliasService;
+
+       @Autowired
+       private CollectionService collectionService;
+
+       @BeforeAll
+       void setupCollection() throws Exception {
+               CollectionCreationResult created = 
collectionService.createCollection(TEST_COLLECTION, null, null, null);
+               assertThat(created.success()).as("Collection creation should 
succeed: %s", created.message()).isTrue();
+               CollectionCreationResult created2 = 
collectionService.createCollection(TEST_COLLECTION_2, null, null, null);
+               assertThat(created2.success()).as("Collection 2 creation should 
succeed: %s", created2.message()).isTrue();
+       }
+
+       @Test
+       @Order(1)
+       void listAliases_initiallyEmpty() throws Exception {
+               Map<String, String> aliases = aliasService.listAliases();
+               // No aliases pointing to our test collection initially
+               assertThat(aliases).doesNotContainKey(TEST_ALIAS);
+       }
+
+       @Test
+       @Order(2)
+       void createAlias_createsNewAlias() throws Exception {
+               AliasResult result = aliasService.createAlias(TEST_ALIAS, 
TEST_COLLECTION);
+
+               assertThat(result.aliasName()).isEqualTo(TEST_ALIAS);
+               assertThat(result.collections()).isEqualTo(TEST_COLLECTION);
+               assertThat(result.success()).isTrue();
+               assertThat(result.timestamp()).isNotNull();
+       }
+
+       @Test
+       @Order(3)
+       void listAliases_containsCreatedAlias() throws Exception {
+               Map<String, String> aliases = aliasService.listAliases();
+
+               assertThat(aliases).containsEntry(TEST_ALIAS, TEST_COLLECTION);
+       }
+
+       @Test
+       @Order(4)
+       void createAlias_updatesExistingAlias() throws Exception {
+               // Update alias to point to a different collection
+               AliasResult result = aliasService.createAlias(TEST_ALIAS, 
TEST_COLLECTION_2);
+
+               assertThat(result.success()).isTrue();
+               assertThat(result.collections()).isEqualTo(TEST_COLLECTION_2);
+
+               // Verify the alias now points to the second collection
+               Map<String, String> aliases = aliasService.listAliases();
+               assertThat(aliases).containsEntry(TEST_ALIAS, 
TEST_COLLECTION_2);
+       }
+
+       @Test
+       @Order(5)
+       void createAlias_multipleAliasesCanExist() throws Exception {
+               // Assert only on aliases this test creates itself. Asserting 
on TEST_ALIAS
+               // here would couple the expectation to whichever collection an 
earlier
+               // test last repointed it at.
+               AliasResult first = aliasService.createAlias(TEST_ALIAS_2, 
TEST_COLLECTION);
+               AliasResult second = aliasService.createAlias(TEST_ALIAS_3, 
TEST_COLLECTION);
+               assertThat(first.success()).isTrue();
+               assertThat(second.success()).isTrue();
+
+               Map<String, String> aliases = aliasService.listAliases();
+               assertThat(aliases).containsEntry(TEST_ALIAS_2, 
TEST_COLLECTION).containsEntry(TEST_ALIAS_3, TEST_COLLECTION);
+
+               // TEST_ALIAS_2 is left in place for the deletion tests that 
follow;
+               // TEST_ALIAS_3 is this test's own fixture, so clean it up here.
+               
assertThat(aliasService.deleteAlias(TEST_ALIAS_3).success()).isTrue();
+       }
+
+       @Test
+       @Order(6)
+       void deleteAlias_removesAlias() throws Exception {
+               AliasResult result = aliasService.deleteAlias(TEST_ALIAS);
+
+               assertThat(result.aliasName()).isEqualTo(TEST_ALIAS);
+               assertThat(result.collections()).isNull();
+               assertThat(result.success()).isTrue();
+
+               // Verify it's gone
+               Map<String, String> aliases = aliasService.listAliases();
+               assertThat(aliases).doesNotContainKey(TEST_ALIAS);
+               // But the other alias still exists
+               assertThat(aliases).containsEntry(TEST_ALIAS_2, 
TEST_COLLECTION);
+       }
+
+       @Test
+       @Order(7)
+       void deleteAlias_cleanupSecondAlias() throws Exception {
+               AliasResult result = aliasService.deleteAlias(TEST_ALIAS_2);
+               assertThat(result.success()).isTrue();
+
+               Map<String, String> aliases = aliasService.listAliases();
+               assertThat(aliases).doesNotContainKey(TEST_ALIAS_2);
+       }
+}
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java 
b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java
new file mode 100644
index 00000000..bf1f3262
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.common.util.NamedList;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+
+/**
+ * Unit tests for {@link AliasService}.
+ */
+@DisabledInNativeImage
+class AliasServiceTest {
+
+       private SolrClient solrClient;
+       private AliasService aliasService;
+
+       @BeforeEach
+       void setUp() {
+               solrClient = mock(SolrClient.class);
+               aliasService = new AliasService(solrClient);
+       }
+
+       @Nested
+       @DisplayName("list-aliases")
+       class ListAliases {
+
+               @Test
+               @DisplayName("returns aliases map when aliases exist")
+               void returnsAliasesWhenPresent() throws Exception {
+                       // Given
+                       NamedList<Object> responseData = new NamedList<>();
+                       responseData.add("aliases", Map.of("ORDERS", 
"ORDERS_V2", "PRODUCTS", "PRODUCTS_V1"));
+                       when(solrClient.request(any(), 
nullable(String.class))).thenReturn(responseData);
+
+                       // When
+                       Map<String, String> result = aliasService.listAliases();
+
+                       // Then
+                       assertThat(result).containsEntry("ORDERS", 
"ORDERS_V2").containsEntry("PRODUCTS", "PRODUCTS_V1").hasSize(2);
+               }
+
+               @Test
+               @DisplayName("returns empty map when no aliases exist")
+               void returnsEmptyMapWhenNoAliases() throws Exception {
+                       // Given
+                       NamedList<Object> responseData = new NamedList<>();
+                       when(solrClient.request(any(), 
nullable(String.class))).thenReturn(responseData);
+
+                       // When
+                       Map<String, String> result = aliasService.listAliases();
+
+                       // Then
+                       assertThat(result).isEmpty();
+               }
+       }
+
+       @Nested
+       @DisplayName("create-alias")
+       class CreateAlias {
+
+               @Test
+               @DisplayName("creates alias successfully")
+               void createsAliasSuccessfully() throws Exception {
+                       // Given
+                       NamedList<Object> responseData = new NamedList<>();
+                       responseData.add("responseHeader", new 
NamedList<>(Map.of("status", 0)));
+                       when(solrClient.request(any(), 
nullable(String.class))).thenReturn(responseData);
+
+                       // When
+                       AliasResult result = aliasService.createAlias("ORDERS", 
"ORDERS_V2");
+
+                       // Then
+                       assertThat(result.aliasName()).isEqualTo("ORDERS");
+                       assertThat(result.collections()).isEqualTo("ORDERS_V2");
+                       assertThat(result.success()).isTrue();
+                       assertThat(result.message()).contains("successfully");
+                       assertThat(result.timestamp()).isNotNull();
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when alias name 
is blank")
+               void throwsWhenAliasNameBlank() {
+                       assertThatThrownBy(() -> aliasService.createAlias("", 
"ORDERS_V2"))
+                                       
.isInstanceOf(IllegalArgumentException.class).hasMessage("Alias name must not 
be blank");
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when alias name 
is null")
+               void throwsWhenAliasNameNull() {
+                       assertThatThrownBy(() -> aliasService.createAlias(null, 
"ORDERS_V2"))
+                                       
.isInstanceOf(IllegalArgumentException.class).hasMessage("Alias name must not 
be blank");
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when collections 
is blank")
+               void throwsWhenCollectionsBlank() {
+                       assertThatThrownBy(() -> 
aliasService.createAlias("ORDERS", ""))
+                                       
.isInstanceOf(IllegalArgumentException.class).hasMessage("Collections must not 
be blank");
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when collections 
is null")
+               void throwsWhenCollectionsNull() {
+                       assertThatThrownBy(() -> 
aliasService.createAlias("ORDERS", null))
+                                       
.isInstanceOf(IllegalArgumentException.class).hasMessage("Collections must not 
be blank");
+               }
+       }
+
+       @Nested
+       @DisplayName("delete-alias")
+       class DeleteAlias {
+
+               @Test
+               @DisplayName("deletes alias successfully")
+               void deletesAliasSuccessfully() throws Exception {
+                       // Given
+                       NamedList<Object> responseData = new NamedList<>();
+                       responseData.add("responseHeader", new 
NamedList<>(Map.of("status", 0)));
+                       when(solrClient.request(any(), 
nullable(String.class))).thenReturn(responseData);
+
+                       // When
+                       AliasResult result = aliasService.deleteAlias("ORDERS");
+
+                       // Then
+                       assertThat(result.aliasName()).isEqualTo("ORDERS");
+                       assertThat(result.collections()).isNull();
+                       assertThat(result.success()).isTrue();
+                       assertThat(result.message()).contains("deleted");
+                       assertThat(result.timestamp()).isNotNull();
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when alias name 
is blank")
+               void throwsWhenAliasNameBlank() {
+                       assertThatThrownBy(() -> 
aliasService.deleteAlias("")).isInstanceOf(IllegalArgumentException.class)
+                                       .hasMessage("Alias name must not be 
blank");
+               }
+
+               @Test
+               @DisplayName("throws IllegalArgumentException when alias name 
is null")
+               void throwsWhenAliasNameNull() {
+                       assertThatThrownBy(() -> 
aliasService.deleteAlias(null)).isInstanceOf(IllegalArgumentException.class)
+                                       .hasMessage("Alias name must not be 
blank");
+               }
+       }
+}

Reply via email to