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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## 1.70.136

- **Seven more response fields the Dashboard API returns are now mapped**, each of them a nested
object that needed a new model class. As with the scalars in 1.70.135, callers who set
`JsonMissingMemberHandling.ThrowOnError` were getting a failed deserialization rather than an
ignored field:
`TwoPointFourGhzSettings.Dot11ax` and `FiveGhzSettings.Dot11ax`, `SwitchPort.PerpetualPoe` and
`.FastPoe`, `NetworkApplianceSsidRadiusServer.Radsec`, `Admin.OtherOrganizationAccounts`, and
`VpnBgp.Ipv6` and `.TunnelDownTermination`.

Ten new classes back them: `TwoPointFourGhzSettingsDot11ax`, `FiveGhzSettingsDot11ax`,
`SwitchPortPerpetualPoe`, `SwitchPortFastPoe`, `NetworkApplianceSsidRadiusServerRadsec`,
`AdminOtherOrganizationAccounts` and `AdminOtherOrganizationAccountsLockout`, `VpnBgpIpv6`,
`VpnBgpIpv6SinglePeering` and `VpnBgpTunnelDownTermination`.

`Dot11ax` on both bands and both PoE objects are in the v1.74.0 OpenAPI spec on request bodies as
well as responses, so they are writable: `Dot11ax` is read/write and the two PoE objects are
read/update, matching their endpoints. The spec also marks `axEnabled` deprecated in favour of
`dot11ax.enabled` on both bands, which the XML docs now say. `Radsec`,
`OtherOrganizationAccounts`, `Ipv6` and `TunnelDownTermination` appear nowhere in the spec and are
mapped read-only from the observed responses.

- **These mappings have credential-free regression tests** in
`Meraki.Api.Test.Data.MissingMemberNestedFieldTests`, deserializing the observed payloads (with
identifying values replaced) using `MissingMemberHandling.Error`. The CI step added in 1.70.135
runs them.

## 1.70.135

- **Seven more response fields the Dashboard API returns are now mapped**, all of them simple
Expand Down
224 changes: 224 additions & 0 deletions Meraki.Api.Test/Data/MissingMemberNestedFieldTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Meraki.Api.Test.Data;

/// <summary>
/// Regression tests for the nested response objects that live organizations were observed to return
/// with no matching model property. Each payload keeps the shape of a real Dashboard API response
/// with identifying values replaced, and each test deserializes it with the same settings
/// <see cref="JsonMissingMemberHandling.ThrowOnError"/> uses, so an unmapped field fails the test
/// rather than being silently dropped.
/// </summary>
public class MissingMemberNestedFieldTests
{
private static readonly JsonSerializerSettings ThrowOnMissingMember = new()
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error,
Converters = [new StringEnumConverter()]
};

private static T Deserialize<T>(string json)
{
var result = JsonConvert.DeserializeObject<T>(json, ThrowOnMissingMember);
_ = result.Should().NotBeNull();
return result!;
}

// twoFourGhzSettings from GET /networks/{networkId}/wireless/rfProfiles
[Fact]
public void Deserialize_TwoPointFourGhzSettings_MapsDot11ax()
{
const string json = """
{
"maxPower": 30,
"minPower": 5,
"minBitrate": 11,
"rxsop": null,
"validAutoChannels": [ 1, 6, 11 ],
"axEnabled": true,
"dot11ax": {
"enabled": true
}
}
""";

var settings = Deserialize<TwoPointFourGhzSettings>(json);

_ = settings.Dot11ax.Should().NotBeNull();
_ = settings.Dot11ax!.Enabled.Should().BeTrue();
}

// fiveGhzSettings from GET /networks/{networkId}/wireless/rfProfiles
[Fact]
public void Deserialize_FiveGhzSettings_MapsDot11ax()
{
const string json = """
{
"maxPower": 30,
"minPower": 8,
"minBitrate": 12,
"rxsop": null,
"validAutoChannels": [ 36, 40, 44, 48, 149, 153, 157, 161 ],
"channelWidth": "auto",
"dot11ax": {
"enabled": true
}
}
""";

var settings = Deserialize<FiveGhzSettings>(json);

_ = settings.Dot11ax.Should().NotBeNull();
_ = settings.Dot11ax!.Enabled.Should().BeTrue();
}

// GET /devices/{serial}/switch/ports
[Fact]
public void Deserialize_SwitchPort_MapsPerpetualPoeAndFastPoe()

Check warning on line 79 in Meraki.Api.Test/Data/MissingMemberNestedFieldTests.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Meraki.Api.Test/Data/MissingMemberNestedFieldTests.cs#L79

Method MissingMemberNestedFieldTests::Deserialize_SwitchPort_MapsPerpetualPoeAndFastPoe has 52 lines of code (limit is 50)
{
const string json = """
{
"portId": "1",
"name": "Access Port",
"tags": [],
"enabled": true,
"poeEnabled": true,
"perpetualPoe": {
"enabled": false
},
"fastPoe": {
"enabled": false
},
"type": "trunk",
"vlan": 1,
"voiceVlan": null,
"allowedVlans": "all",
"activeVlans": "1-4094",
"isolationEnabled": false,
"rstpEnabled": true,
"stpGuard": "disabled",
"linkNegotiation": "Auto negotiate",
"portScheduleId": null,
"schedule": null,
"udld": "Alert only",
"linkNegotiationCapabilities": [
"Auto negotiate",
"1 Gigabit full duplex (auto)"
],
"accessPolicyType": "Open",
"daiTrusted": false,
"profile": {
"enabled": false,
"id": "",
"iname": null
},
"module": {
"model": null
},
"mirror": {
"mode": "Not mirroring traffic"
}
}
""";

var port = Deserialize<SwitchPort>(json);

_ = port.PerpetualPoe.Should().NotBeNull();
_ = port.PerpetualPoe!.Enabled.Should().BeFalse();
_ = port.FastPoe.Should().NotBeNull();
_ = port.FastPoe!.Enabled.Should().BeFalse();
}

// radiusServers from GET /networks/{networkId}/appliance/ssids
[Fact]
public void Deserialize_NetworkApplianceSsidRadiusServer_MapsRadsec()
{
const string json = """
{
"host": "192.0.2.10",
"port": 2654,
"id": "1234567890123456789",
"openRoamingCertificateId": null,
"caCertificate": null,
"radsec": {
"enabled": false,
"tlsIdleTimeout": 900
}
}
""";

var radiusServer = Deserialize<NetworkApplianceSsidRadiusServer>(json);

_ = radiusServer.Radsec.Should().NotBeNull();
_ = radiusServer.Radsec!.Enabled.Should().BeFalse();
_ = radiusServer.Radsec.TlsIdleTimeout.Should().Be(900);
}

// GET /organizations/{organizationId}/admins
[Fact]
public void Deserialize_Admin_MapsOtherOrganizationAccounts()
{
const string json = """
{
"id": "1234567890123456789",
"name": "Example Administrator",
"email": "admin@example.com",
"authenticationMethod": "Email",
"orgAccess": "full",
"accountStatus": "ok",
"otherOrganizationAccounts": {
"lockout": {
"isLocked": false
}
},
"twoFactorAuthEnabled": false,
"hasApiKey": true,
"lastActive": "2026-09-04T10:23:23Z",
"networks": [],
"tags": []
}
""";

var admin = Deserialize<Admin>(json);

_ = admin.OtherOrganizationAccounts.Should().NotBeNull();
_ = admin.OtherOrganizationAccounts!.Lockout.Should().NotBeNull();
_ = admin.OtherOrganizationAccounts.Lockout!.IsLocked.Should().BeFalse();
}

// GET /networks/{networkId}/appliance/vpn/bgp - the whole observed response, including the
// PriorityRoute scalar mapped alongside these two nested objects.
[Fact]
public void Deserialize_VpnBgp_MapsIpv6AndTunnelDownTermination()
{
const string json = """
{
"enabled": false,
"ibgpHoldTimer": 240,
"asNumber": 64512,
"routerId": null,
"priorityRoute": "Auto VPN",
"ipv6": {
"singlePeering": {
"enabled": false
}
},
"tunnelDownTermination": {
"enabled": null
},
"neighbors": []
}
""";

var bgp = Deserialize<VpnBgp>(json);

_ = bgp.PriorityRoute.Should().Be("Auto VPN");
_ = bgp.Ipv6.Should().NotBeNull();
_ = bgp.Ipv6!.SinglePeering.Should().NotBeNull();
_ = bgp.Ipv6.SinglePeering!.Enabled.Should().BeFalse();
_ = bgp.TunnelDownTermination.Should().NotBeNull();
_ = bgp.TunnelDownTermination!.Enabled.Should().BeNull();
}
}
7 changes: 7 additions & 0 deletions Meraki.Api/Data/Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,11 @@ public class Admin : NamedIdentifiedItem
[ApiAccess(ApiAccess.ReadCreate)]
[DataMember(Name = "camera")]
public List<AdminCameraAccess>? Camera { get; set; }

/// <summary>
/// The administrator's accounts in other organizations - Undocumented, observed in responses only
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "otherOrganizationAccounts")]
public AdminOtherOrganizationAccounts? OtherOrganizationAccounts { get; set; }
}
15 changes: 15 additions & 0 deletions Meraki.Api/Data/AdminOtherOrganizationAccounts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Meraki.Api.Data;

/// <summary>
/// The administrator's accounts in other organizations - Undocumented, observed in responses only
/// </summary>
[DataContract]
public class AdminOtherOrganizationAccounts
{
/// <summary>
/// The lockout state of the administrator's accounts in other organizations
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "lockout")]
public AdminOtherOrganizationAccountsLockout? Lockout { get; set; }
}
15 changes: 15 additions & 0 deletions Meraki.Api/Data/AdminOtherOrganizationAccountsLockout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Meraki.Api.Data;

/// <summary>
/// The lockout state of an administrator's accounts in other organizations
/// </summary>
[DataContract]
public class AdminOtherOrganizationAccountsLockout
{
/// <summary>
/// Whether the administrator is locked out of their accounts in other organizations
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "isLocked")]
public bool? IsLocked { get; set; }
}
8 changes: 8 additions & 0 deletions Meraki.Api/Data/FiveGhzSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,16 @@ public class FiveGhzSettings

/// <summary>
/// Determines whether 802.11ax is enabled. Undocumented property.
/// Deprecated by the API in favour of <see cref="Dot11ax"/>.
/// </summary>
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "axEnabled")]
public bool? AxEnabled { get; set; }

/// <summary>
/// IEEE 802.11ax (Wi-Fi 6) settings.
/// </summary>
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "dot11ax")]
public FiveGhzSettingsDot11ax? Dot11ax { get; set; }
}
15 changes: 15 additions & 0 deletions Meraki.Api/Data/FiveGhzSettingsDot11ax.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Meraki.Api.Data;

/// <summary>
/// IEEE 802.11ax (Wi-Fi 6) settings for the 5Ghz band
/// </summary>
[DataContract]
public class FiveGhzSettingsDot11ax
{
/// <summary>
/// Determines whether IEEE 802.11ax (Wi-Fi 6) on the 5 GHz band is enabled. Can be either true or false. If false, we highly recommend disabling band steering. If omitted, the existing 5 GHz IEEE 802.11ax setting is preserved.
/// </summary>
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "enabled")]
public bool? Enabled { get; set; }
}
7 changes: 7 additions & 0 deletions Meraki.Api/Data/NetworkApplianceSsidRadiusServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,11 @@ public class NetworkApplianceSsidRadiusServer
[ApiAccess(ApiAccess.ReadWrite)]
[DataMember(Name = "secret")]
public string? Secret { get; set; }

/// <summary>
/// RADSEC settings for the RADIUS server - Undocumented, observed in responses only
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "radsec")]
public NetworkApplianceSsidRadiusServerRadsec? Radsec { get; set; }
}
22 changes: 22 additions & 0 deletions Meraki.Api/Data/NetworkApplianceSsidRadiusServerRadsec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Meraki.Api.Data;

/// <summary>
/// RADSEC settings for an appliance SSID RADIUS server - Undocumented, observed in responses only
/// </summary>
[DataContract]
public class NetworkApplianceSsidRadiusServerRadsec
{
/// <summary>
/// Whether RADSEC is enabled for the RADIUS server
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "enabled")]
public bool? Enabled { get; set; }

/// <summary>
/// The interval (in seconds) that determines how long a TLS session can remain idle for a RADSec server before it is automatically terminated
/// </summary>
[ApiAccess(ApiAccess.Read)]
[DataMember(Name = "tlsIdleTimeout")]
public int? TlsIdleTimeout { get; set; }
}
14 changes: 14 additions & 0 deletions Meraki.Api/Data/SwitchPort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ public class SwitchPort : NamedItem
[DataMember(Name = "poeEnabled")]
public bool PoeEnabled { get; set; }

/// <summary>
/// Perpetual PoE settings for the switch port.
/// </summary>
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "perpetualPoe")]
public SwitchPortPerpetualPoe? PerpetualPoe { get; set; }

/// <summary>
/// Fast PoE settings for the switch port.
/// </summary>
[ApiAccess(ApiAccess.ReadUpdate)]
[DataMember(Name = "fastPoe")]
public SwitchPortFastPoe? FastPoe { get; set; }

/// <summary>
/// The type of the switch port ('trunk' or 'access')
/// </summary>
Expand Down
Loading