mirror of
https://github.com/microsoft/mcp.git
synced 2026-11-04 12:01:31 +00:00
* Add Azure IoT Hub routing endpoint health and diagnostics tools Provide a current endpoint-health snapshot and historical routing evidence, including downstream resource existence and per-metric availability without inferring overall hub health. Use a dedicated routing service with cloud-aware ARM validation, SDK-default retries, bounded operations, and cancellation handling. Include command documentation, registration, unit tests, recorded tests, and routing test infrastructure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten IoT Hub routing metric fallback and ARM link validation Only retry batched target metrics individually when Azure Monitor rejects the batch with 400, the incompatible-metric case. Throttling and server errors now fail the batch after SDK retries instead of fanning out into per-metric requests that consume the operation budget. Validate each ARM request URI, including nextLink continuations, before resolving the tenant or credential so untrusted destinations fail before any authentication work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use IAzureService endpoint validation for IoT Hub routing ARM requests #3884 removed the static EndpointValidator overload that took an executing tool namespace, so the routing service no longer compiles against main (CS1739). Validate ARM request and nextLink URIs through IAzureService.ValidateAzureServiceEndpoint, as IoTHubDeviceService now does. Tests use AzureServiceTestHelpers so endpoint validation stays real and shared ARM clients route through the test handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Resolve IoT Hub routing targets only from the endpoint URI Key-based endpoints have no endpointUri, and IoT Hub endpoint ids are opaque GUIDs rather than ARM resource IDs. The mapper fell back to the entity path or container name as the namespace or account name, so diagnostics checked a resource that does not exist and reported an existing target as notFound. Derive the target name only from the endpoint URI host so these endpoints report an unresolved target, and use opaque ids in the mapper fixture to match service responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document unresolved IoT Hub routing targets for key-based endpoints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Configure IoT Hub live test routing and devices in the post script The live pipeline failed in test-resources-post.ps1 because the azure-iot CLI extension it used cannot be installed on network-isolated agents. Seed the test devices through the IoT Hub registry REST API with Microsoft Entra ID instead, and grant the test principal IoT Hub Data Contributor. Also add the identity-based Service Bus routing endpoints in the post script rather than the template. IoT Hub validates those endpoints when they are created and rejects them (IH400117) until the routing identity's Service Bus Data Sender assignment takes effect. ARM cannot wait for that, so the post script retries until it does. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate IoT Hub resource names before building ARM resource IDs Hub and resource group names were interpolated into ARM resource IDs. ResourceIdentifier accepts traversal segments, and URI normalization then sent the request elsewhere: rg1/../rg2 and rg1\..\rg2 read the hub from resource group rg2, and .. escaped to the subscription scope. Reject names outside the Azure naming rules before any credential or network work. The check runs in the shared routing service path used by both routing commands, and in IoTHubService.GetIoTHub, which serves hub get and, through the hostname resolver, the device and query tools. Resource group names follow the documented rule: 1-90 letters or digits (Char.IsLetterOrDigit), underscores, hyphens, periods, or parentheses, not ending with a period. Routing targets built from the hub's endpoint configuration are reported as unresolved unless every name is a single path segment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
108 lines
4.2 KiB
C#
108 lines
4.2 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using Azure.Mcp.Tools.IoTHub.Models;
|
|
using Azure.Mcp.Tools.IoTHub.Routing;
|
|
using Xunit;
|
|
|
|
namespace Azure.Mcp.Tools.IoTHub.Tests.Routing;
|
|
|
|
public class RoutingEndpointMapperTests()
|
|
{
|
|
[Fact]
|
|
public void ConvertToRoutingEndpointDetailsList_HandlesAbsentEndpoints()
|
|
{
|
|
Assert.Empty(RoutingEndpointMapper.ConvertToRoutingEndpointDetailsList(null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToRoutingEndpointDetailsList_PreservesAllEndpointKinds()
|
|
{
|
|
var endpoint = new RoutingEndpointProperties
|
|
{
|
|
Id = "dbc3cb4d-0119-4d46-9824-e8e3e83fb04a",
|
|
Name = "endpoint1",
|
|
SubscriptionId = "11111111-1111-1111-1111-111111111111",
|
|
ResourceGroup = "rg1",
|
|
EndpointUri = "https://account1.blob.core.windows.net",
|
|
EntityPath = "entity1",
|
|
ContainerName = "container1",
|
|
DatabaseName = "database1",
|
|
AuthenticationType = "identityBased",
|
|
BatchFrequencyInSeconds = 60
|
|
};
|
|
var endpoints = new RoutingEndpoints
|
|
{
|
|
EventHubs = [endpoint],
|
|
ServiceBusQueues = [endpoint],
|
|
ServiceBusTopics = [endpoint],
|
|
StorageContainers = [endpoint],
|
|
CosmosDBSqlContainers = [endpoint]
|
|
};
|
|
|
|
var result = RoutingEndpointMapper.ConvertToRoutingEndpointDetailsList(endpoints);
|
|
|
|
Assert.Equal(
|
|
["EventHub", "ServiceBusQueue", "ServiceBusTopic", "StorageContainer", "CosmosDBSqlContainer"],
|
|
result.Select(value => value.EndpointType));
|
|
Assert.All(result, value =>
|
|
{
|
|
Assert.Equal(endpoint.Id, value.EndpointId);
|
|
Assert.Equal(endpoint.Name, value.Name);
|
|
Assert.Equal(endpoint.SubscriptionId, value.SubscriptionId);
|
|
Assert.Equal(endpoint.ResourceGroup, value.ResourceGroup);
|
|
Assert.Equal(endpoint.EndpointUri, value.EndpointUri);
|
|
Assert.Equal(endpoint.AuthenticationType, value.AuthenticationType);
|
|
Assert.Equal(60, value.BatchFrequencyInSeconds);
|
|
Assert.Equal("account1", value.EndpointResourceName);
|
|
});
|
|
Assert.All(result.Take(3), value =>
|
|
{
|
|
Assert.Equal("entity1", value.EntityPath);
|
|
Assert.Null(value.ContainerName);
|
|
Assert.Null(value.DatabaseName);
|
|
});
|
|
Assert.All(result.Skip(3), value =>
|
|
{
|
|
Assert.Equal("container1", value.ContainerName);
|
|
Assert.Null(value.EntityPath);
|
|
});
|
|
Assert.Null(result[3].DatabaseName);
|
|
Assert.Equal("database1", result[4].DatabaseName);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToRoutingEndpointDetailsList_LeavesKeyBasedTargetNameUnresolved()
|
|
{
|
|
// IoT Hub returns an opaque endpoint id and no endpointUri for key-based endpoints.
|
|
static RoutingEndpointProperties KeyBased(string? entityPath = null, string? containerName = null) => new()
|
|
{
|
|
Id = "dbc3cb4d-0119-4d46-9824-e8e3e83fb04a",
|
|
Name = "endpoint1",
|
|
SubscriptionId = "11111111-1111-1111-1111-111111111111",
|
|
ResourceGroup = "rg1",
|
|
EntityPath = entityPath,
|
|
ContainerName = containerName,
|
|
AuthenticationType = "keyBased"
|
|
};
|
|
var endpoints = new RoutingEndpoints
|
|
{
|
|
EventHubs = [KeyBased(entityPath: "telemetry-hub")],
|
|
ServiceBusQueues = [KeyBased(entityPath: "telemetry-queue")],
|
|
ServiceBusTopics = [KeyBased(entityPath: "telemetry-topic")],
|
|
StorageContainers = [KeyBased(containerName: "telemetry")]
|
|
};
|
|
|
|
var result = RoutingEndpointMapper.ConvertToRoutingEndpointDetailsList(endpoints);
|
|
|
|
Assert.Equal(4, result.Count);
|
|
Assert.All(result, value =>
|
|
{
|
|
Assert.Null(value.EndpointResourceName);
|
|
Assert.Equal("dbc3cb4d-0119-4d46-9824-e8e3e83fb04a", value.EndpointId);
|
|
});
|
|
Assert.Equal(["telemetry-hub", "telemetry-queue", "telemetry-topic"], result.Take(3).Select(value => value.EntityPath));
|
|
Assert.Equal("telemetry", result[3].ContainerName);
|
|
}
|
|
}
|