Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<T>()` 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`,
Expand Down
14 changes: 7 additions & 7 deletions Meraki.Api.Test/MerakiClientQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"<https://api.meraki.com/api/v1/organizations/123/devices?perPage=1000&startingAfter=Q2XX>; 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",
"<https://api.meraki.com/api/v1/x?startingAfter=A>; rel=first, <https://api.meraki.com/api/v1/x?endingBefore=B>; 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();
}
}
75 changes: 75 additions & 0 deletions Meraki.Api.Test/MerakiClientSectionWiringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Meraki.Api.Test;

/// <summary>
/// Unit tests confirming that the organization-level Refit clients reported in
/// <see href="https://github.com/panoramicdata/Meraki.Api/issues/337">issue 337</see>
/// are assigned by the <see cref="MerakiClient"/> constructor. These require no network
/// and no API key, because the wiring happens entirely in the constructor.
/// </summary>
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();
}

/// <summary>
/// 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".
/// </summary>
[Fact]
public void Constructor_SetsOrganizationsWirelessDevicesChannelUtilization()
{
using var merakiClient = CreateClient();

_ = merakiClient.Organizations.Wireless.Devices.ChannelUtilization.Should().NotBeNull();
}

/// <summary>
/// The sections that were previously wired must keep working.
/// </summary>
[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();
}
}
2 changes: 1 addition & 1 deletion Meraki.Api.Test/Networks/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion Meraki.Api.Test/Organizations/Networks/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,48 @@ public interface IOrganizationsWirelessDevicesChannelUtilization
/// Get average channel utilization for all bands in a network, split by AP
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
/// <param name="orgnanizationId"></param>
/// <param name="organizationId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationByDevice")]
[Get("/organizations/{organizationId}/wireless/devices/channelUtilization/byDevice")]
Task<List<OrganizationWirelessDevicesChannelUtilizationByDeviceItem>> GetOrganizationWirelessDevicesChannelUtilizationsByDeviceAsync(string orgnanizationId, CancellationToken cancellationToken = default);
Task<List<OrganizationWirelessDevicesChannelUtilizationByDeviceItem>> GetOrganizationWirelessDevicesChannelUtilizationsByDeviceAsync(string organizationId, CancellationToken cancellationToken = default);

/// <summary>
/// Get average channel utilization across all bands for all networks in the organization
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
/// <param name="orgnanizationId"></param>
/// <param name="organizationId"></param>
/// <param name="networkId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationByNetwork")]
[Get("/organizations/{organizationId}/wireless/devices/channelUtilization/byNetwork")]
Task<List<OrganizationWirelessDevicesChannelUtilizationByNetworkItem>> GetOrganizationWirelessDevicesChannelUtilizationsByNetworkAsync(string orgnanizationId, string networkId, CancellationToken cancellationToken = default);
Task<List<OrganizationWirelessDevicesChannelUtilizationByNetworkItem>> GetOrganizationWirelessDevicesChannelUtilizationsByNetworkAsync(string organizationId, string networkId, CancellationToken cancellationToken = default);

/// <summary>
/// Get a time-series of average channel utilization for all bands, segmented by device.
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
/// <param name="orgnanizationId"></param>
/// <param name="organizationId"></param>
/// <param name="networkId"></param>
/// <param name="interval"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByInterval")]
[Get("/organizations/{organizationId}/wireless/devices/channelUtilization/history/byDevice/byInterval")]
Task<List<OrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalItem>> GetOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalAsync(string orgnanizationId, string networkId, string interval, CancellationToken cancellationToken = default);
Task<List<OrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalItem>> GetOrganizationWirelessDevicesChannelUtilizationHistoryByDeviceByIntervalAsync(string organizationId, string networkId, string interval, CancellationToken cancellationToken = default);

/// <summary>
/// Get a time-series of average channel utilization for all bands
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
/// <param name="orgnanizationId"></param>
/// <param name="organizationId"></param>
/// <param name="networkId"></param>
/// <param name="interval"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[ApiOperationId("getOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByInterval")]
[Get("/organizations/{organizationId}/wireless/devices/channelUtilization/history/byNetwork/byInterval")]
Task<List<OrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalItem>> GetOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalAsync(string orgnanizationId, string networkId, string interval, CancellationToken cancellationToken = default);
Task<List<OrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalItem>> GetOrganizationWirelessDevicesChannelUtilizationHistoryByNetworkByIntervalAsync(string organizationId, string networkId, string interval, CancellationToken cancellationToken = default);
}
19 changes: 18 additions & 1 deletion Meraki.Api/MerakiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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),
Expand All @@ -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)
}
}
};

Expand Down
Loading