From ee24614abcf47a1b7fe51c6281b60466a2b9c46b Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 3 Feb 2022 09:56:58 -0500 Subject: [PATCH 1/3] (GH-168) Allow one participant to use more than one device --- .../Utilities/ParticipantDeviceConverter.cs | 140 ++++++++++++++++++ Source/ZoomNet/Models/DashboardParticipant.cs | 2 +- Source/ZoomNet/Models/ParticipantDevice.cs | 8 +- .../Utilities/ParticipantDeviceConverter.cs | 94 ++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs create mode 100644 Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs new file mode 100644 index 00000000..421637e9 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs @@ -0,0 +1,140 @@ +using Newtonsoft.Json; +using Shouldly; +using System; +using System.IO; +using System.Text; +using Xunit; +using ZoomNet.Models; +using ZoomNet.Utilities; + +namespace StrongGrid.UnitTests.Utilities +{ + public class ParticipantDeviceConverterTests + { + [Fact] + public void Properties() + { + // Act + var converter = new ParticipantDeviceConverter(); + + // Assert + converter.CanRead.ShouldBeTrue(); + converter.CanWrite.ShouldBeTrue(); + } + + [Fact] + public void CanConvert() + { + // Act + var converter = new ParticipantDeviceConverter(); + + // Assert + converter.CanConvert(typeof(string)).ShouldBeTrue(); + } + + [Fact] + public void Write_single() + { + // Arrange + var sb = new StringBuilder(); + var sw = new StringWriter(sb); + var writer = new JsonTextWriter(sw); + + var value = new[] + { + ParticipantDevice.Windows + }; + + var serializer = new JsonSerializer(); + + var converter = new ParticipantDeviceConverter(); + + // Act + converter.WriteJson(writer, value, serializer); + var result = sb.ToString(); + + // Assert + result.ShouldBe("\"Windows\""); + } + [Fact] + public void Write_multiple() + { + // Arrange + var sb = new StringBuilder(); + var sw = new StringWriter(sb); + var writer = new JsonTextWriter(sw); + + var value = new[] + { + ParticipantDevice.Unknown, + ParticipantDevice.Phone + }; + + var serializer = new JsonSerializer(); + + var converter = new ParticipantDeviceConverter(); + + // Act + converter.WriteJson(writer, value, serializer); + var result = sb.ToString(); + + // Assert + result.ShouldBe("\"Unknown + Phone\""); + } + + [Fact] + public void Read_single() + { + // Arrange + var json = "'Phone'"; + + var textReader = new StringReader(json); + var jsonReader = new JsonTextReader(textReader); + var objectType = (Type)null; + var existingValue = (object)null; + var serializer = new JsonSerializer(); + + var converter = new ParticipantDeviceConverter(); + + // Act + jsonReader.Read(); + var result = converter.ReadJson(jsonReader, objectType, existingValue, serializer); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeOfType(); + + var resultAsArray = (ParticipantDevice[])result; + resultAsArray.Length.ShouldBe(1); + resultAsArray[0].ShouldBe(ParticipantDevice.Phone); + } + + [Fact] + public void Read_multiple() + { + // Arrange + var json = "'Unknown + Phone'"; + + var textReader = new StringReader(json); + var jsonReader = new JsonTextReader(textReader); + var objectType = (Type)null; + var existingValue = (object)null; + var serializer = new JsonSerializer(); + + var converter = new ParticipantDeviceConverter(); + + // Act + jsonReader.Read(); + var result = converter.ReadJson(jsonReader, objectType, existingValue, serializer); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeOfType(); + + var resultAsArray = (ParticipantDevice[])result; + resultAsArray.Length.ShouldBe(2); + resultAsArray[0].ShouldBe(ParticipantDevice.Unknown); + resultAsArray[1].ShouldBe(ParticipantDevice.Phone); + } + } +} diff --git a/Source/ZoomNet/Models/DashboardParticipant.cs b/Source/ZoomNet/Models/DashboardParticipant.cs index d6fea68e..22b442b3 100644 --- a/Source/ZoomNet/Models/DashboardParticipant.cs +++ b/Source/ZoomNet/Models/DashboardParticipant.cs @@ -44,7 +44,7 @@ public class DashboardParticipant /// The type of device using which the participant joined the meeting. /// [JsonProperty(PropertyName = "device")] - public ParticipantDevice Device { get; set; } + public ParticipantDevice[] Devices { get; set; } /// /// Gets or sets the participant’s IP address. diff --git a/Source/ZoomNet/Models/ParticipantDevice.cs b/Source/ZoomNet/Models/ParticipantDevice.cs index bb558743..f5749d25 100644 --- a/Source/ZoomNet/Models/ParticipantDevice.cs +++ b/Source/ZoomNet/Models/ParticipantDevice.cs @@ -1,19 +1,19 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using System.Runtime.Serialization; +using ZoomNet.Utilities; namespace ZoomNet.Models { /// /// Enumeration to indicate the type of device a participant used to join a meeting. /// - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(ParticipantDeviceConverter))] public enum ParticipantDevice { /// - /// Unknown + /// Unknown. /// - [EnumMember(Value = "")] + [EnumMember(Value = "Unknown")] Unknown, /// diff --git a/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs new file mode 100644 index 00000000..67301b4d --- /dev/null +++ b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using ZoomNet.Models; + +namespace ZoomNet.Utilities +{ + /// + /// Converts a JSON string into and array of devices. + /// + /// + internal class ParticipantDeviceConverter : JsonConverter + { + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// + /// true if this can read JSON; otherwise, false. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets a value indicating whether this can write JSON. + /// + /// + /// true if this can write JSON; otherwise, false. + /// + public override bool CanWrite + { + get { return true; } + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var stringValue = string.Join(" + ", ((IEnumerable)value).Select(device => device.ToEnumString())); + serializer.Serialize(writer, stringValue); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + /// Unable to determine the field type. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var stringValue = (string)reader.Value; + var items = stringValue + .Split(new[] { "+" }, StringSplitOptions.RemoveEmptyEntries) + .Select(item => Convert(item)) + .ToArray(); + + return items; + } + + throw new Exception("Unable to convert to ParticipantDevice"); + } + + private static ParticipantDevice Convert(string deviceAsString) + { + if (string.IsNullOrWhiteSpace(deviceAsString)) return ParticipantDevice.Unknown; + return deviceAsString.Trim().ToEnum(); + } + } +} From 5971fc61685d9fb44d769e68be0b0f382db881b6 Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 3 Feb 2022 10:05:47 -0500 Subject: [PATCH 2/3] (GH-168) Extension methods to convert an enum to string and a string to its corresponding enum item --- .../ZoomNet.UnitTests/Extensions/Internal.cs | 2 +- Source/ZoomNet/Extensions/Internal.cs | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Source/ZoomNet.UnitTests/Extensions/Internal.cs b/Source/ZoomNet.UnitTests/Extensions/Internal.cs index 46e4dfa7..cf9fca6d 100644 --- a/Source/ZoomNet.UnitTests/Extensions/Internal.cs +++ b/Source/ZoomNet.UnitTests/Extensions/Internal.cs @@ -5,7 +5,7 @@ namespace ZoomNet.UnitTests.Utilities { - public class InternalTests + public class InternalExtensionsTests { [Fact] public void GetProperty_when_property_is_present_and_throwIfMissing_is_true() diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 2f16c65d..30a3820f 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -3,12 +3,14 @@ using Pathoschild.Http.Client; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; +using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -712,6 +714,49 @@ internal static void Replace(this ICollection collection, T oldValue, T ne } } + /// Convert an enum to its string representation. + /// The enum type. + /// The value. + /// The string representation of the enum value. + /// Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions . + internal static string ToEnumString(this T enumValue) + where T : Enum + { + var enumMemberAttribute = enumValue.GetAttributeOfType(); + if (enumMemberAttribute != null) return enumMemberAttribute.Value; + + var descriptionAttribute = enumValue.GetAttributeOfType(); + if (descriptionAttribute != null) return descriptionAttribute.Description; + + return enumValue.ToString(); + } + + /// Parses a string into its corresponding enum value. + /// The enum type. + /// The string value. + /// The enum representation of the string value. + /// Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions . + internal static T ToEnum(this string str) + where T : Enum + { + var enumType = typeof(T); + foreach (var name in Enum.GetNames(enumType)) + { + var customAttributes = enumType.GetField(name).GetCustomAttributes(true); + + // See if there's a matching 'EnumMember' attribute + if (customAttributes.OfType().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase))) return (T)Enum.Parse(enumType, name); + + // See if there's a matching 'Description' attribute + if (customAttributes.OfType().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase))) return (T)Enum.Parse(enumType, name); + + // See if the value matches the name + if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase)) return (T)Enum.Parse(enumType, name); + } + + throw new ArgumentException($"There is no value in the {enumType.Name} enum that corresponds to '{str}'."); + } + /// Asynchronously converts the JSON encoded content and convert it to an object of the desired type. /// The response model to deserialize into. /// The content. From da40bd4c87ba5b4b85b49700ac6b29c66c7f2979 Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 3 Feb 2022 19:29:14 -0500 Subject: [PATCH 3/3] (GH-168) Fix issue when parsing a single device Also, more unit tests --- .../Models/DashboardParticipant.cs | 61 +++++++++++++++++++ .../Utilities/ParticipantDeviceConverter.cs | 15 +++-- Source/ZoomNet/Models/DashboardParticipant.cs | 6 +- .../Utilities/ParticipantDeviceConverter.cs | 2 +- 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs diff --git a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs new file mode 100644 index 00000000..16733aa5 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using Shouldly; +using Xunit; +using ZoomNet.Models; + +namespace StrongGrid.UnitTests.Resources +{ + public class DashboardParticipantTests + { + #region FIELDS + + internal const string SINGLE_DASHBOARDPARTICIPANT_JSON = @"{ + 'id': 'd52f19c548b88490b5d16fcbd38', + 'user_id': '32dsfsd4g5gd', + 'user_name': 'dojo', + 'device': 'Unknown', + 'ip_address': '127.0.0.1', + 'location': 'New York', + 'network_type': 'Wired', + 'microphone': 'Plantronics BT600', + 'camera': 'FaceTime HD Camera', + 'speaker': 'Plantronics BT600', + 'data_center': 'SC', + 'full_data_center': 'United States;United States (US03_SC CRC)', + 'connection_type': 'P2P', + 'join_time': '2019-09-07T13:15:02.837Z', + 'leave_time': '2019-09-07T13:15:09.837Z', + 'share_application': false, + 'share_desktop': true, + 'share_whiteboard': true, + 'recording': false, + 'status': 'in_waiting_room', + 'pc_name': 'dojo\'s pc', + 'domain': 'Dojo-workspace', + 'mac_addr': ' 00:0a:95:9d:68:16', + 'harddisk_id': 'sed proident in', + 'version': '4.4.55383.0716', + 'leave_reason': 'Dojo left the meeting.
Reason: Host ended the meeting.', + 'sip_uri': 'sip:sipp@10.100.112.140:11029', + 'from_sip_uri': 'sip:sipp@10.100.112.140:11029', + 'role': 'panelist' + }"; + + #endregion + + [Fact] + public void Parse_json() + { + // Arrange + + // Act + var result = JsonConvert.DeserializeObject(SINGLE_DASHBOARDPARTICIPANT_JSON); + + // Assert + result.ShouldNotBeNull(); + result.Devices.ShouldNotBeNull(); + result.Devices.Length.ShouldBe(1); + result.Devices[0].ShouldBe(ParticipantDevice.Unknown); + } + } +} diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs index 421637e9..79191ad4 100644 --- a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs @@ -82,11 +82,18 @@ public void Write_multiple() result.ShouldBe("\"Unknown + Phone\""); } - [Fact] - public void Read_single() + [Theory] + [InlineDataAttribute("", ParticipantDevice.Unknown)] + [InlineDataAttribute("Unknown", ParticipantDevice.Unknown)] + [InlineDataAttribute("Android", ParticipantDevice.Android)] + [InlineDataAttribute("Phone", ParticipantDevice.Phone)] + [InlineDataAttribute("iOs", ParticipantDevice.IOS)] + [InlineDataAttribute("H.323/SIP", ParticipantDevice.Sip)] + [InlineDataAttribute("Windows", ParticipantDevice.Windows)] + public void Read_single(string value, ParticipantDevice expectedValue) { // Arrange - var json = "'Phone'"; + var json = $"'{value}'"; var textReader = new StringReader(json); var jsonReader = new JsonTextReader(textReader); @@ -106,7 +113,7 @@ public void Read_single() var resultAsArray = (ParticipantDevice[])result; resultAsArray.Length.ShouldBe(1); - resultAsArray[0].ShouldBe(ParticipantDevice.Phone); + resultAsArray[0].ShouldBe(expectedValue); } [Fact] diff --git a/Source/ZoomNet/Models/DashboardParticipant.cs b/Source/ZoomNet/Models/DashboardParticipant.cs index 22b442b3..45ca2447 100644 --- a/Source/ZoomNet/Models/DashboardParticipant.cs +++ b/Source/ZoomNet/Models/DashboardParticipant.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System; +using ZoomNet.Utilities; namespace ZoomNet.Models { @@ -38,12 +39,13 @@ public class DashboardParticipant public string UserName { get; set; } /// - /// Gets or sets the type of device using which the participant joined the meeting. + /// Gets or sets the device(s) used by the participant to join the meeting. /// /// - /// The type of device using which the participant joined the meeting. + /// The type of device used by the participant to join the meeting. /// [JsonProperty(PropertyName = "device")] + [JsonConverter(typeof(ParticipantDeviceConverter))] public ParticipantDevice[] Devices { get; set; } /// diff --git a/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs index 67301b4d..21b0fdd0 100644 --- a/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs @@ -75,7 +75,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { var stringValue = (string)reader.Value; var items = stringValue - .Split(new[] { "+" }, StringSplitOptions.RemoveEmptyEntries) + .Split(new[] { '+' }) .Select(item => Convert(item)) .ToArray();