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.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 new file mode 100644 index 00000000..79191ad4 --- /dev/null +++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs @@ -0,0 +1,147 @@ +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\""); + } + + [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 = $"'{value}'"; + + 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(expectedValue); + } + + [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/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. diff --git a/Source/ZoomNet/Models/DashboardParticipant.cs b/Source/ZoomNet/Models/DashboardParticipant.cs index d6fea68e..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,13 +39,14 @@ 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")] - public ParticipantDevice Device { get; set; } + [JsonConverter(typeof(ParticipantDeviceConverter))] + 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..21b0fdd0 --- /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[] { '+' }) + .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(); + } + } +}