From ee88a43cf05a8ab3f519faac771cd0826406b13c Mon Sep 17 00:00:00 2001 From: Roland Banks Date: Mon, 3 Aug 2026 10:30:54 +0700 Subject: [PATCH 1/2] Wire up missing organization Refit clients (fixes #337) The MerakiClient constructor never assigned several organization-level Refit clients, so they were still null after construction and threw NullReferenceException on first use: Organizations.Summary.SwitchPower Organizations.Appliance.Uplinks.Usage Organizations.Wireless.Devices.Latency Organizations.Wireless.Devices.PacketLoss Organizations.Wireless.Devices.ChannelUtilization IOrganizationsWirelessDevicesChannelUtilization additionally could not be built by Refit at all. All four of its methods spelled the parameter "orgnanizationId" while the route templates used {organizationId}, so RestService.For() threw: ArgumentException: URL /organizations/{organizationId}/wireless/devices/ channelUtilization/byDevice 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, so nothing could have called it. Adds MerakiClientSectionWiringTests, which needs no network and no API key because the wiring happens entirely in the constructor. This closes the five cases in #337 only. A reflection census of the section tree shows 82 further Refit clients and 4 sub-sections are still left null by the constructor; that is tracked separately in #357. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 17 +++++ .../MerakiClientSectionWiringTests.cs | 75 +++++++++++++++++++ ...ationsWirelessDevicesChannelUtilization.cs | 16 ++-- Meraki.Api/MerakiClient.cs | 19 ++++- 4 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 Meraki.Api.Test/MerakiClientSectionWiringTests.cs 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/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/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) + } } }; From 5f920af993b3da6b0ba4fdd3bd394bb84eb65c44 Mon Sep 17 00:00:00 2001 From: Roland Banks Date: Mon, 3 Aug 2026 10:31:02 +0700 Subject: [PATCH 2/2] Fix IDE0058 violations in the test project .editorconfig sets csharp_style_unused_value_expression_statement_preference to discard_variable, which surfaces as IDE0058 in the IDE. Nine statements in the test project discarded their value implicitly. They now use the explicit discard, matching the rest of the test project. Behaviour is unchanged; this is a style-only fix. Co-Authored-By: Claude Opus 5 --- Meraki.Api.Test/MerakiClientQueryTests.cs | 14 +++++++------- Meraki.Api.Test/Networks/Tests.cs | 2 +- Meraki.Api.Test/Organizations/Networks/Tests.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) 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/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);