diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4f2d50..fddd712d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.70.42 + +- Fixed several organization-level Refit clients being left `null` by the `MerakiClient` + constructor, which threw `NullReferenceException` on first use + (issue [#337](https://github.com/panoramicdata/Meraki.Api/issues/337)): + - `Organizations.Summary.SwitchPower` + - `Organizations.Appliance.Uplinks.Usage` + - `Organizations.Wireless.Devices.Latency` + - `Organizations.Wireless.Devices.PacketLoss` + - `Organizations.Wireless.Devices.ChannelUtilization` +- Fixed `IOrganizationsWirelessDevicesChannelUtilization`, which Refit could not build at + all: all four methods spelled the parameter `orgnanizationId` while the route templates + used `{organizationId}`, so `RestService.For()` threw + `ArgumentException: ... has parameter organizationid, but no method parameter matches`. + The parameter is now `organizationId` throughout. Callers using named arguments would + need to update, but the interface could not be constructed before this fix. + ## 1.70.37 - Surfaced `MerakiClient.Wireless.DataRateHistory.GetNetworkWirelessDataRateHistoryAsync`, diff --git a/Meraki.Api.Test/MerakiClientQueryTests.cs b/Meraki.Api.Test/MerakiClientQueryTests.cs index 5516f295..bbc0fe8d 100644 --- a/Meraki.Api.Test/MerakiClientQueryTests.cs +++ b/Meraki.Api.Test/MerakiClientQueryTests.cs @@ -17,38 +17,38 @@ public class MerakiClientQueryTests public void BuildRequestUri_PreservesApiV1SegmentAndCombinesPath(string baseAddress, string path, string expected) { var uri = MerakiClient.BuildRequestUri(new Uri(baseAddress), path); - uri.AbsoluteUri.Should().Be(expected); + _ = uri.AbsoluteUri.Should().Be(expected); } [Fact] public void GetNextPageUri_WithRelNext_ReturnsAbsoluteNextUriWithCursor() { using var response = new HttpResponseMessage(); - response.Headers.TryAddWithoutValidation( + _ = response.Headers.TryAddWithoutValidation( "Link", "; rel=next"); var next = MerakiClient.GetNextPageUri(response.Headers); - next.Should().NotBeNull(); - System.Web.HttpUtility.ParseQueryString(next!.Query).Get("startingAfter").Should().Be("Q2XX"); + _ = next.Should().NotBeNull(); + _ = System.Web.HttpUtility.ParseQueryString(next!.Query).Get("startingAfter").Should().Be("Q2XX"); } [Fact] public void GetNextPageUri_WithFirstAndPrevButNoNext_ReturnsNull() { using var response = new HttpResponseMessage(); - response.Headers.TryAddWithoutValidation( + _ = response.Headers.TryAddWithoutValidation( "Link", "; rel=first, ; rel=prev"); - MerakiClient.GetNextPageUri(response.Headers).Should().BeNull(); + _ = MerakiClient.GetNextPageUri(response.Headers).Should().BeNull(); } [Fact] public void GetNextPageUri_WithNoLinkHeader_ReturnsNull() { using var response = new HttpResponseMessage(); - MerakiClient.GetNextPageUri(response.Headers).Should().BeNull(); + _ = MerakiClient.GetNextPageUri(response.Headers).Should().BeNull(); } } diff --git a/Meraki.Api.Test/MerakiClientSectionWiringTests.cs b/Meraki.Api.Test/MerakiClientSectionWiringTests.cs new file mode 100644 index 00000000..b0ade62e --- /dev/null +++ b/Meraki.Api.Test/MerakiClientSectionWiringTests.cs @@ -0,0 +1,75 @@ +namespace Meraki.Api.Test; + +/// +/// Unit tests confirming that the organization-level Refit clients reported in +/// issue 337 +/// are assigned by the constructor. These require no network +/// and no API key, because the wiring happens entirely in the constructor. +/// +public class MerakiClientSectionWiringTests +{ + private static MerakiClient CreateClient() + => new(new MerakiClientOptions + { + ApiKey = "0000000000000000000000000000000000000000", + UserAgent = "Meraki.Api.Test/1.0" + }); + + [Fact] + public void Constructor_SetsOrganizationsSummarySwitchPower() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Summary.SwitchPower.Should().NotBeNull(); + } + + [Fact] + public void Constructor_SetsOrganizationsApplianceUplinksUsage() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Appliance.Uplinks.Usage.Should().NotBeNull(); + } + + [Fact] + public void Constructor_SetsOrganizationsWirelessDevicesLatency() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Wireless.Devices.Latency.Should().NotBeNull(); + } + + [Fact] + public void Constructor_SetsOrganizationsWirelessDevicesPacketLoss() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Wireless.Devices.PacketLoss.Should().NotBeNull(); + } + + /// + /// This one additionally proves the Refit route/parameter fix: before it, building the + /// interface threw ArgumentException because the route placeholder was {organizationId} + /// but the method parameter was spelled "orgnanizationId". + /// + [Fact] + public void Constructor_SetsOrganizationsWirelessDevicesChannelUtilization() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Wireless.Devices.ChannelUtilization.Should().NotBeNull(); + } + + /// + /// The sections that were previously wired must keep working. + /// + [Fact] + public void Constructor_StillSetsPreviouslyWiredOrganizationClients() + { + using var merakiClient = CreateClient(); + + _ = merakiClient.Organizations.Summary.Top.Should().NotBeNull(); + _ = merakiClient.Organizations.SwitchPortsOverview.Should().NotBeNull(); + _ = merakiClient.Organizations.Uplinks.Should().NotBeNull(); + } +} diff --git a/Meraki.Api.Test/Networks/Tests.cs b/Meraki.Api.Test/Networks/Tests.cs index 27fde7d7..ace1b6af 100644 --- a/Meraki.Api.Test/Networks/Tests.cs +++ b/Meraki.Api.Test/Networks/Tests.cs @@ -212,7 +212,7 @@ await TestMerakiClient _ = updatedVlan.Should().NotBeNull(); //--- Claim/Remove device - await TestMerakiClient + _ = await TestMerakiClient .Networks .Devices .ClaimNetworkDevicesAsync(newNetwork.Id, true, new DeviceClaimRequest { Serials = [Configuration.TestDeviceSerial] }, cancellationToken: CancellationToken); diff --git a/Meraki.Api.Test/Organizations/Networks/Tests.cs b/Meraki.Api.Test/Organizations/Networks/Tests.cs index 993f01d0..8c561904 100644 --- a/Meraki.Api.Test/Organizations/Networks/Tests.cs +++ b/Meraki.Api.Test/Organizations/Networks/Tests.cs @@ -230,7 +230,7 @@ await TestMerakiClient _ = updatedVlan.Should().NotBeNull(); //--- Claim/Remove device - await TestMerakiClient + _ = await TestMerakiClient .Networks .Devices .ClaimNetworkDevicesAsync(newNetwork.Id, true, new DeviceClaimRequest { Serials = [Configuration.TestDeviceSerial] }, cancellationToken: CancellationToken); diff --git a/Meraki.Api/Interfaces/General/Organizations/IOrganizationsWirelessDevicesChannelUtilization.cs b/Meraki.Api/Interfaces/General/Organizations/IOrganizationsWirelessDevicesChannelUtilization.cs index 55de9a3d..764b4416 100644 --- a/Meraki.Api/Interfaces/General/Organizations/IOrganizationsWirelessDevicesChannelUtilization.cs +++ b/Meraki.Api/Interfaces/General/Organizations/IOrganizationsWirelessDevicesChannelUtilization.cs @@ -9,48 +9,48 @@ public interface IOrganizationsWirelessDevicesChannelUtilization /// Get average channel utilization for all bands in a network, split by AP /// /// Thrown when fails to make API call - /// + /// /// /// [ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationByDevice")] [Get("/organizations/{organizationId}/wireless/devices/channelUtilization/byDevice")] - Task> GetOrganizationWirelessDevicesChannelUtilizationsByDeviceAsync(string orgnanizationId, CancellationToken cancellationToken = default); + Task> GetOrganizationWirelessDevicesChannelUtilizationsByDeviceAsync(string organizationId, CancellationToken cancellationToken = default); /// /// Get average channel utilization across all bands for all networks in the organization /// /// Thrown when fails to make API call - /// + /// /// /// /// [ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationByNetwork")] [Get("/organizations/{organizationId}/wireless/devices/channelUtilization/byNetwork")] - Task> GetOrganizationWirelessDevicesChannelUtilizationsByNetworkAsync(string orgnanizationId, string networkId, CancellationToken cancellationToken = default); + Task> GetOrganizationWirelessDevicesChannelUtilizationsByNetworkAsync(string organizationId, string networkId, CancellationToken cancellationToken = default); /// /// Get a time-series of average channel utilization for all bands, segmented by device. /// /// Thrown when fails to make API call - /// + /// /// /// /// /// [ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByInterval")] [Get("/organizations/{organizationId}/wireless/devices/channelUtilization/history/byDevice/byInterval")] - Task> GetOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalAsync(string orgnanizationId, string networkId, string interval, CancellationToken cancellationToken = default); + Task> GetOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalAsync(string organizationId, string networkId, string interval, CancellationToken cancellationToken = default); /// /// Get a time-series of average channel utilization for all bands /// /// Thrown when fails to make API call - /// + /// /// /// /// /// [ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByInterval")] [Get("/organizations/{organizationId}/wireless/devices/channelUtilization/history/byNetwork/byInterval")] - Task> GetOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalAsync(string orgnanizationId, string networkId, string interval, CancellationToken cancellationToken = default); + Task> GetOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalAsync(string organizationId, string networkId, string interval, CancellationToken cancellationToken = default); } diff --git a/Meraki.Api/MerakiClient.cs b/Meraki.Api/MerakiClient.cs index e6dbbb7e..c58f84e1 100644 --- a/Meraki.Api/MerakiClient.cs +++ b/Meraki.Api/MerakiClient.cs @@ -143,6 +143,13 @@ public MerakiClient(MerakiClientOptions options, ILogger? logger = default) } }, ApiRequests = RefitFor(Organizations.ApiRequests), + Appliance = new() + { + Uplinks = new() + { + Usage = RefitFor(Organizations.Appliance.Uplinks.Usage) + } + }, ApplianceSecurityEvents = RefitFor(Organizations.ApplianceSecurityEvents), AssuranceAlerts = RefitFor(Organizations.AssuranceAlerts), BrandingPolicies = new() @@ -239,7 +246,8 @@ public MerakiClient(MerakiClientOptions options, ILogger? logger = default) Splash = RefitFor(Organizations.Splash), Summary = new() { - Top = RefitFor(Organizations.Summary.Top) + Top = RefitFor(Organizations.Summary.Top), + SwitchPower = RefitFor(Organizations.Summary.SwitchPower) }, Switches = RefitFor(Organizations.Switches), SwitchPortsOverview = RefitFor(Organizations.SwitchPortsOverview), @@ -250,6 +258,15 @@ public MerakiClient(MerakiClientOptions options, ILogger? logger = default) Logs = RefitFor(Organizations.Webhooks.Logs), PayloadTemplates = RefitFor(Organizations.Webhooks.PayloadTemplates), HttpServers = RefitFor(Organizations.Webhooks.HttpServers) + }, + Wireless = new() + { + Devices = new() + { + ChannelUtilization = RefitFor(Organizations.Wireless.Devices.ChannelUtilization), + Latency = RefitFor(Organizations.Wireless.Devices.Latency), + PacketLoss = RefitFor(Organizations.Wireless.Devices.PacketLoss) + } } };