ResourceResolverServiceTests.cs•11.5 kB
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.ClientModel.Primitives;
using System.Text;
using System.Text.Json;
using Azure;
using Azure.Core;
using Azure.ResourceManager.Resources;
using AzureMcp.Core.Options;
using AzureMcp.Core.Services.Azure.Subscription;
using AzureMcp.Core.Services.Azure.Tenant;
using AzureMcp.Monitor.Services;
using NSubstitute;
using Xunit;
namespace AzureMcp.Monitor.UnitTests.Metrics;
public class ResourceResolverServiceTests
{
    private readonly ISubscriptionService _subscriptionService;
    private readonly ITenantService _tenantService;
    private readonly ResourceResolverService _service;
    private readonly SubscriptionResource _subscriptionResource = Substitute.For<SubscriptionResource>();
    public ResourceResolverServiceTests()
    {
        _subscriptionService = Substitute.For<ISubscriptionService>();
        _tenantService = Substitute.For<ITenantService>();
        _service = new ResourceResolverService(_subscriptionService, _tenantService);
        _subscriptionService.GetSubscription(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<RetryPolicyOptions?>())
            .Returns(_subscriptionResource);
    }
    #region Constructor Tests
    [Fact]
    public void Constructor_WithValidParameters_Succeeds()
    {
        // Act & Assert - Constructor should not throw
        var service = new ResourceResolverService(_subscriptionService, _tenantService);
        Assert.NotNull(service);
    }
    [Fact]
    public void Constructor_WithNullSubscriptionService_ThrowsArgumentNullException()
    {
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => new ResourceResolverService(null!, _tenantService));
    }
    #endregion
    #region ResolveResourceIdAsync Tests
    [Fact]
    public async Task ResolveResourceIdAsync_WithFullResourceId_ReturnsDirectly()
    {
        // Arrange
        var fullResourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test";
        var subscription = "87654321-4321-4321-4321-210987654321"; // Different subscription to ensure it's not used
        // Act
        var result = await _service.ResolveResourceIdAsync(subscription, null, null, fullResourceId);
        // Assert
        Assert.Equal(fullResourceId, result.ToString());
        // Verify that subscription service was not called since we're passing a full resource ID
        await _subscriptionService.DidNotReceive().GetSubscription(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<RetryPolicyOptions?>());
    }
    [Fact]
    public async Task ResolveResourceIdAsync_WithResourceGroupAndType_BuildsDirectPath()
    {
        // Arrange
        var subscription = "12345678-1234-1234-1234-123456789012";
        var resourceGroup = "test-rg";
        var resourceType = "Microsoft.Storage/storageAccounts";
        var resourceName = "test";
        var expectedResourceId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/{resourceType}/{resourceName}";
        // Act
        var result = await _service.ResolveResourceIdAsync(subscription, resourceGroup, resourceType, resourceName);
        // Assert
        Assert.Equal(expectedResourceId, result);
        await _subscriptionService.DidNotReceive().GetSubscription(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<RetryPolicyOptions?>());
    }
    [Theory]
    [InlineData("", "")]
    public async Task ResolveResourceIdAsync_WithNullOrEmptySubscription_ThrowsArgumentException(string? subscription, string? resourceName)
    {
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentException>(() =>
            _service.ResolveResourceIdAsync(subscription!, null, null, resourceName!));
    }
    [Theory]
    [InlineData(null, null)]
    public async Task ResolveResourceIdAsync_WithNullOrEmptySubscription_ThrowsArgumentNullException(string? subscription, string? resourceName)
    {
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentNullException>(() =>
            _service.ResolveResourceIdAsync(subscription!, null, null, resourceName!));
    }
    [Fact]
    public async Task ResolveResourceIdAsync_ResourceDiscovery_NoResourcesFound_ThrowsException()
    {
        // Arrange
        var subscription = "sub1";
        var resourceName = "nonexistent-resource";
        var emptyAsyncPageable = CreateEmptyAsyncPageable();
        _subscriptionResource.GetGenericResourcesAsync(cancellationToken: Arg.Any<CancellationToken>()).Returns(emptyAsyncPageable);
        // Act & Assert
        var exception = await Assert.ThrowsAsync<Exception>(() =>
            _service.ResolveResourceIdAsync(subscription, null, null, resourceName));
        Assert.Contains($"Resource '{resourceName}' not found in subscription '{subscription}'", exception.Message);
    }
    [Fact]
    public async Task ResolveResourceIdAsync_ResourceDiscovery_MultipleResourcesFound_ThrowsException()
    {
        // Arrange
        var subscription = Guid.NewGuid().ToString();
        var resourceName = "duplicate-resource";
        var resource1 = CreateMockGenericResource($"/subscriptions/{subscription}/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/{resourceName}", "rg1", "Microsoft.Storage/storageAccounts", resourceName);
        var resource2 = CreateMockGenericResource($"/subscriptions/{subscription}/resourceGroups/rg2/providers/Microsoft.Compute/virtualMachines/{resourceName}", "rg2", "Microsoft.Compute/virtualMachines", resourceName);
        var resourcesAsyncPageable = CreateAsyncPageableWithItems(resource1, resource2);
        _subscriptionResource.GetGenericResourcesAsync(cancellationToken: Arg.Any<CancellationToken>()).Returns(resourcesAsyncPageable);
        // Act & Assert
        var exception = await Assert.ThrowsAsync<Exception>(() =>
            _service.ResolveResourceIdAsync(subscription, null, null, resourceName));
        Assert.Contains($"Multiple resources named '{resourceName}' found", exception.Message);
        Assert.Contains("Please specify both resourceGroup and resourceType parameters", exception.Message);
    }
    [Fact]
    public async Task ResolveResourceIdAsync_ResourceDiscovery_SingleResourceFound_ReturnsResourceId()
    {
        // Arrange
        var subscription = Guid.NewGuid().ToString();
        var resourceName = "unique-resource";
        var expectedResourceId = $"/subscriptions/{subscription}/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/{resourceName}";
        var resource = CreateMockGenericResource(expectedResourceId, "rg1", "Microsoft.Storage/storageAccounts", resourceName);
        var subscriptionResource = Substitute.For<SubscriptionResource>();
        var resourcesAsyncPageable = CreateAsyncPageableWithItems(resource);
        subscriptionResource.GetGenericResourcesAsync(cancellationToken: Arg.Any<CancellationToken>()).Returns(resourcesAsyncPageable);
        _subscriptionService.GetSubscription(subscription, Arg.Any<string?>(), Arg.Any<RetryPolicyOptions?>())
            .Returns(subscriptionResource);
        // Act
        var result = await _service.ResolveResourceIdAsync(subscription, null, null, resourceName);
        // Assert
        Assert.Equal(expectedResourceId, result.ToString());
    }
    [Fact]
    public async Task ResolveResourceIdAsync_WithResourceGroupFilter_FiltersCorrectly()
    {
        // Arrange
        var subscription = Guid.NewGuid().ToString();
        var resourceGroup = "rg1";
        var resourceName = "filtered-resource";
        var expectedResourceId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/{resourceName}";
        // Create resources in different resource groups with same name
        var resource1 = CreateMockGenericResource(expectedResourceId, "rg1", "Microsoft.Storage/storageAccounts", resourceName);
        var resource2 = CreateMockGenericResource($"/subscriptions/{subscription}/resourceGroups/rg2/providers/Microsoft.Storage/storageAccounts/{resourceName}", "rg2", "Microsoft.Storage/storageAccounts", resourceName);
        var resourcesAsyncPageable = CreateAsyncPageableWithItems(resource1, resource2);
        _subscriptionResource.GetGenericResourcesAsync(cancellationToken: Arg.Any<CancellationToken>()).Returns(resourcesAsyncPageable);
        // Act
        var result = await _service.ResolveResourceIdAsync(subscription, resourceGroup, null, resourceName);
        // Assert
        Assert.Equal(expectedResourceId, result.ToString());
    }
    [Fact]
    public async Task ResolveResourceIdAsync_WithResourceTypeFilter_FiltersCorrectly()
    {
        // Arrange
        var subscription = Guid.NewGuid().ToString();
        var resourceType = "Microsoft.Storage/storageAccounts";
        var resourceName = "filtered-resource";
        var expectedResourceId = $"/subscriptions/{subscription}/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/{resourceName}";
        // Create resources of different types with same name
        var resource1 = CreateMockGenericResource(expectedResourceId, "rg1", "Microsoft.Storage/storageAccounts", resourceName);
        var resource2 = CreateMockGenericResource($"/subscriptions/{subscription}/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/{resourceName}", "rg1", "Microsoft.Compute/virtualMachines", resourceName);
        var resourcesAsyncPageable = CreateAsyncPageableWithItems(resource1, resource2);
        _subscriptionResource.GetGenericResourcesAsync(cancellationToken: Arg.Any<CancellationToken>()).Returns(resourcesAsyncPageable);
        // Act
        var result = await _service.ResolveResourceIdAsync(subscription, null, resourceType, resourceName);
        // Assert
        Assert.Equal(expectedResourceId, result.ToString());
    }
    #endregion
    #region Helper Methods
    private static GenericResource CreateMockGenericResource(string resourceId, string resourceGroupName, string resourceType, string resourceName)
    {
        var result = Substitute.For<GenericResource>();
        var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes($@"{{
                ""name"": ""{resourceName}"",
                ""id"": ""{resourceId}"",
                ""type"": ""{resourceType}""
            }}"));
        IJsonModel<GenericResourceData> data = new GenericResourceData(AzureLocation.AustraliaCentral);
        var d = data.Create(ref reader, new ModelReaderWriterOptions("W"));
        result.Data.Returns(d);
        result.Id.Returns(new ResourceIdentifier(resourceId));
        return result;
    }
    private static AsyncPageable<GenericResource> CreateEmptyAsyncPageable()
    {
        return CreateAsyncPageableWithItems();
    }
    private static AsyncPageable<GenericResource> CreateAsyncPageableWithItems(params GenericResource[] items)
    {
        var page = Page<GenericResource>.FromValues(items, continuationToken: null, Substitute.For<Response>());
        return AsyncPageable<GenericResource>.FromPages(new[]
        {
            page
        });
    }
    #endregion
}
// Helper class to create async enumerables for testing
public static class AsyncPageableHelper
{
    public static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T> items)
    {
        foreach (var item in items)
        {
            yield return item;
        }
        await Task.CompletedTask;
    }
}