diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs index f851048e..fcd9b8c5 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs @@ -11,7 +11,7 @@ namespace ZoomNet.IntegrationTests.Tests; public class Chatbot : IIntegrationTest { - /// + /// public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, CancellationToken cancellationToken) { var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); diff --git a/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs b/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs index c64ea1a9..77fdfb4b 100644 --- a/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs +++ b/Source/ZoomNet.UnitTests/Models/PhoneCallUserProfileTests.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using Shouldly; +using Shouldly; +using System.Text.Json; using Xunit; using ZoomNet.Json; using ZoomNet.Models; diff --git a/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs b/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs new file mode 100644 index 00000000..35eceacf --- /dev/null +++ b/Source/ZoomNet.UnitTests/Models/PhoneUserTests.cs @@ -0,0 +1,84 @@ +using Shouldly; +using System.Text.Json; +using Xunit; +using ZoomNet.Json; +using ZoomNet.Models; + +namespace ZoomNet.UnitTests.Models +{ + public class PhoneUserTests + { + #region constants + + internal const string PHONE_USER = @"{ + ""id"": ""NL3cEpSdRc-c2t8aLoZqiw"", + ""phone_user_id"": ""u7pnC468TaS46OuNoEw6GA"", + ""email"": ""test_phone_user@testapi.com"", + ""name"": ""test phone user"", + ""extension_id"": ""CcrEGgmeQem1uyJsuIRKwA"", + ""extension_number"": 123, + ""status"": ""activate"", + ""calling_plans"": [ + { + ""type"": 600, + ""name"": ""Delhi billing"", + ""billing_account_id"": ""3WWAEiEjTj2IQuyDiKMd_A"", + ""billing_account_name"": ""Delhi billing"" + } + ], + ""phone_numbers"": [ + { + ""id"": ""---M1padRvSUtw7YihN7sA"", + ""number"": ""14232058798"" + } + ], + ""site"": { + ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"", + ""name"": ""Test Site"" + }, + ""department"": ""Test"", + ""cost_center"": ""Cost Test Center"" + }"; + + #endregion + + #region tests + + [Fact] + public void Parse_Json_PhoneUserTests() + { + // Arrange + + // Act + var result = JsonSerializer.Deserialize( + PHONE_USER, JsonFormatter.SerializerOptions); + + // Assert + result.Id.ShouldBe("NL3cEpSdRc-c2t8aLoZqiw"); + result.PhoneUserId.ShouldBe("u7pnC468TaS46OuNoEw6GA"); + result.Email.ShouldBe("test_phone_user@testapi.com"); + result.Name.ShouldBe("test phone user"); + result.ExtensionId.ShouldBe("CcrEGgmeQem1uyJsuIRKwA"); + result.ExtensionNumber.ShouldBe(123); + result.Status.ShouldBe(PhoneCallUserStatus.Active); + result.CallingPlans.ShouldNotBeNull(); + result.CallingPlans.Length.ShouldBe(1); + result.PhoneNumbers.ShouldNotBeNull(); + result.PhoneNumbers.Length.ShouldBe(1); + result.Department.ShouldBe("Test"); + result.CostCenter.ShouldBe("Cost Test Center"); + result.Site.Id.ShouldBe("8f71O6rWT8KFUGQmJIFAdQ"); + result.Site.Name.ShouldBe("Test Site"); + + var callingPlan = result.CallingPlans[0]; + callingPlan.BillingAccountId.ShouldBe("3WWAEiEjTj2IQuyDiKMd_A"); + callingPlan.BillingAccountName.ShouldBe("Delhi billing"); + + var phoneNumber = result.PhoneNumbers[0]; + phoneNumber.PhoneNumberId.ShouldBe("---M1padRvSUtw7YihN7sA"); + phoneNumber.PhoneNumber.ShouldBe("14232058798"); + } + + #endregion + } +} diff --git a/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs b/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs index c1e44131..5beae3c9 100644 --- a/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/PhoneCallUserProfileTests.cs @@ -1,8 +1,8 @@ -using System.Net.Http; +using RichardSzalay.MockHttp; +using Shouldly; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using RichardSzalay.MockHttp; -using Shouldly; using Xunit; using ZoomNet.Resources; diff --git a/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs b/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs new file mode 100644 index 00000000..20906c0b --- /dev/null +++ b/Source/ZoomNet.UnitTests/Resources/PhoneUserTests.cs @@ -0,0 +1,114 @@ +using RichardSzalay.MockHttp; +using Shouldly; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using ZoomNet.Resources; + +namespace ZoomNet.UnitTests.Resources +{ + public class PhoneUserTests + { + #region constants + + internal const string PHONE_USERS_PAGINATED_OBJECT = @"{ + ""next_page_token"": ""F2qwertyg5eIqRRgC2YMauur8ZHUaJqtS3i"", + ""page_size"": 1, + ""total_records"": 10, + ""users"": [ + { + ""id"": ""NL3cEpSdRc-c2t8aLoZqiw"", + ""phone_user_id"": ""u7pnC468TaS46OuNoEw6GA"", + ""email"": ""test_phone_user@testapi.com"", + ""name"": ""test phone user"", + ""extension_id"": ""CcrEGgmeQem1uyJsuIRKwA"", + ""extension_number"": 123, + ""status"": ""activate"", + ""calling_plans"": [ + { + ""type"": 600, + ""name"": ""Delhi billing"", + ""billing_account_id"": ""3WWAEiEjTj2IQuyDiKMd_A"", + ""billing_account_name"": ""Delhi billing"" + } + ], + ""phone_numbers"": [ + { + ""id"": ""---M1padRvSUtw7YihN7sA"", + ""number"": ""14232058798"" + } + ], + ""site"": { + ""id"": ""8f71O6rWT8KFUGQmJIFAdQ"", + ""name"": ""Test Site"" + }, + ""department"": ""Test"", + ""cost_center"": ""Cost Test Center"" + } + ] + }"; + + #endregion + + #region tests + + [Fact] + public async Task GetPhoneUsersPaginatedResponseTestsAsync() + { + // Arrange + var pageSize = 1; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect( + HttpMethod.Get, + Utils.GetZoomApiUri("phone/users")) + .Respond( + "application/json", + PHONE_USERS_PAGINATED_OBJECT); + + var client = Utils.GetFluentClient(mockHttp); + var phone = new Phone(client); + + // Act + var result = await phone + .ListPhoneUsersAsync(pageSize: pageSize) + .ConfigureAwait(true); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.NextPageToken.ShouldNotBeNullOrEmpty(); + result.PageSize.ShouldBe(1); + result.TotalRecords.ShouldBe(10); + result.Records.ShouldNotBeNull(); + result.Records.Length.ShouldBe(1); + result.Records[0].Id.ShouldBe("NL3cEpSdRc-c2t8aLoZqiw"); + result.Records[0].PhoneNumbers[0].PhoneNumberId.ShouldBe("---M1padRvSUtw7YihN7sA"); + result.Records[0].PhoneNumbers[0].PhoneNumber.ShouldBe("14232058798"); + } + + [Theory] + [InlineData(0)] + [InlineData(101)] + public void InvalidPageSize_GetPhoneUsersPaginatedResponseTests(int pageSize) + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + + var client = Utils.GetFluentClient(mockHttp); + var phone = new Phone(client); + + // Act and Assert + var exception = Assert.Throws(() => phone + .ListPhoneUsersAsync(pageSize: pageSize) + .ConfigureAwait(true)); + + exception.ParamName.ShouldBe(nameof(pageSize)); + exception.Message.ShouldStartWith("Records per page must be between 1 and 100"); + } + + #endregion + } +} diff --git a/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs index b7f1ebb5..12dc1853 100644 --- a/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs +++ b/Source/ZoomNet.UnitTests/Resources/SmsSessionTests.cs @@ -1,7 +1,7 @@ -using System.Net.Http; -using System.Threading.Tasks; using RichardSzalay.MockHttp; using Shouldly; +using System.Net.Http; +using System.Threading.Tasks; using Xunit; using ZoomNet.Resources; diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs index 79ee3779..61b62734 100644 --- a/Source/ZoomNet/Extensions/Internal.cs +++ b/Source/ZoomNet/Extensions/Internal.cs @@ -500,22 +500,22 @@ internal static string EnsureEndsWith(this string value, string suffix) internal static T GetPropertyValue(this JsonElement element, string name, T defaultValue) { - return GetPropertyValue(element, new[] { name }, defaultValue, false); + return element.GetPropertyValue(new[] { name }, defaultValue, false); } internal static T GetPropertyValue(this JsonElement element, string[] names, T defaultValue) { - return GetPropertyValue(element, names, defaultValue, false); + return element.GetPropertyValue(names, defaultValue, false); } internal static T GetPropertyValue(this JsonElement element, string name) { - return GetPropertyValue(element, new[] { name }, default, true); + return element.GetPropertyValue(new[] { name }, default, true); } internal static T GetPropertyValue(this JsonElement element, string[] names) { - return GetPropertyValue(element, names, default, true); + return element.GetPropertyValue(names, default, true); } internal static async Task ForEachAsync(this IEnumerable items, Func> action, int maxDegreeOfParalellism) @@ -644,8 +644,8 @@ internal static IEnumerable> ParseQuerystring(this internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response) { - var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME); - DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo); + var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME); + DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo); return diagnosticInfo; } @@ -687,8 +687,8 @@ internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response) if (rootJsonElement.ValueKind == JsonValueKind.Object) { - errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null; - errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage); + errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? jsonErrorCode.GetInt32() : null; + errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage; if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails)) { var errorDetails = string.Join( @@ -749,7 +749,7 @@ internal static async Task DecompressAsync(this Stream source) internal static string ToEnumString(this T enumValue) where T : Enum { - if (TryToEnumString(enumValue, out string stringValue)) return stringValue; + if (enumValue.TryToEnumString(out string stringValue)) return stringValue; return enumValue.ToString(); } @@ -796,7 +796,7 @@ internal static bool TryToEnumString(this T enumValue, out string stringValue internal static T ToEnum(this string str) where T : Enum { - if (TryToEnum(str, out T enumValue)) return enumValue; + if (str.TryToEnum(out T enumValue)) return enumValue; throw new ArgumentException($"There is no value in the {typeof(T).Name} enum that corresponds to '{str}'."); } @@ -907,7 +907,7 @@ private static async Task AsObject(this HttpContent httpContent, string pr return JsonSerializer.Deserialize(responseContent, options ?? JsonFormatter.DeserializerOptions); } - var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default); + var jsonDoc = JsonDocument.Parse(responseContent, default); if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property)) { return property.ToObject(options); @@ -933,7 +933,7 @@ private static async Task AsRawJsonDocument(this HttpContent httpC { var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false); - var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default); + var jsonDoc = JsonDocument.Parse(responseContent, default); if (string.IsNullOrEmpty(propertyName)) { @@ -943,7 +943,7 @@ private static async Task AsRawJsonDocument(this HttpContent httpC if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property)) { var propertyContent = property.GetRawText(); - return JsonDocument.Parse(propertyContent, (JsonDocumentOptions)default); + return JsonDocument.Parse(propertyContent, default); } else if (throwIfPropertyIsMissing) { @@ -1106,7 +1106,7 @@ private static T GetPropertyValue(this JsonElement element, string[] names, T { var underlyingType = Nullable.GetUnderlyingType(typeOfT); var getElementValue = typeof(Internal) - .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) + .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) .MakeGenericMethod(underlyingType); return (T)getElementValue.Invoke(null, new object[] { property.Value }); @@ -1116,7 +1116,7 @@ private static T GetPropertyValue(this JsonElement element, string[] names, T { var elementType = typeOfT.GetElementType(); var getElementValue = typeof(Internal) - .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) + .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic) .MakeGenericMethod(elementType); var arrayList = new ArrayList(property.Value.GetArrayLength()); diff --git a/Source/ZoomNet/Json/BooleanConverter.cs b/Source/ZoomNet/Json/BooleanConverter.cs index 5b0b5944..e63fb15b 100644 --- a/Source/ZoomNet/Json/BooleanConverter.cs +++ b/Source/ZoomNet/Json/BooleanConverter.cs @@ -14,11 +14,17 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer { switch (reader.TokenType) { + case JsonTokenType.None: + case JsonTokenType.Null: + throw new JsonException($"Unable to convert a null value into a boolean value"); + case JsonTokenType.True: case JsonTokenType.False: return reader.GetBoolean(); + case JsonTokenType.Number: return reader.GetByte() == 1; + default: throw new JsonException($"Unable to convert the content of {reader.TokenType.ToEnumString()} JSON node into a boolean value"); } diff --git a/Source/ZoomNet/Json/DateTimeConverter.cs b/Source/ZoomNet/Json/DateTimeConverter.cs index 593963fa..9eebc008 100644 --- a/Source/ZoomNet/Json/DateTimeConverter.cs +++ b/Source/ZoomNet/Json/DateTimeConverter.cs @@ -13,10 +13,16 @@ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, Jso { switch (reader.TokenType) { + case JsonTokenType.None: + case JsonTokenType.Null: + case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()): + throw new JsonException($"Unable to convert a null value to DateTime"); + case JsonTokenType.String: return reader.GetDateTime(); + default: - throw new Exception($"Unable to convert {reader.TokenType.ToEnumString()} to DateTime"); + throw new JsonException($"Unable to convert {reader.TokenType.ToEnumString()} to DateTime"); } } diff --git a/Source/ZoomNet/Json/KeyValuePairConverter.cs b/Source/ZoomNet/Json/KeyValuePairConverter.cs index b055092a..bc50af19 100644 --- a/Source/ZoomNet/Json/KeyValuePairConverter.cs +++ b/Source/ZoomNet/Json/KeyValuePairConverter.cs @@ -59,7 +59,7 @@ public override KeyValuePair[] Read(ref Utf8JsonReader reader, T return values.ToArray(); } - throw new Exception("Unable to read Key/Value pair"); + throw new JsonException("Unable to read Key/Value pair"); } public override void Write(Utf8JsonWriter writer, KeyValuePair[] value, JsonSerializerOptions options) diff --git a/Source/ZoomNet/Json/MeetingConverter.cs b/Source/ZoomNet/Json/MeetingConverter.cs index aba36e7f..01e31066 100644 --- a/Source/ZoomNet/Json/MeetingConverter.cs +++ b/Source/ZoomNet/Json/MeetingConverter.cs @@ -28,7 +28,7 @@ public override Meeting Read(ref Utf8JsonReader reader, Type typeToConvert, Json case MeetingType.RecurringNoFixedTime: return rootElement.ToObject(options); default: - throw new Exception($"{meetingType} is an unknown meeting type"); + throw new JsonException($"{meetingType} is an unknown meeting type"); } } } diff --git a/Source/ZoomNet/Json/NullableDateTimeConverter.cs b/Source/ZoomNet/Json/NullableDateTimeConverter.cs index 53410a9b..d3faf33a 100644 --- a/Source/ZoomNet/Json/NullableDateTimeConverter.cs +++ b/Source/ZoomNet/Json/NullableDateTimeConverter.cs @@ -9,6 +9,8 @@ namespace ZoomNet.Json /// internal class NullableDateTimeConverter : ZoomNetJsonConverter { + private readonly DateTimeConverter _dateTimeConverter = new DateTimeConverter(); + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) @@ -17,17 +19,16 @@ internal class NullableDateTimeConverter : ZoomNetJsonConverter case JsonTokenType.Null: case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()): return null; - case JsonTokenType.String: - return reader.GetDateTime(); + default: - throw new Exception($"Unable to convert {reader.TokenType.ToEnumString()} to nullable DateTime"); + return _dateTimeConverter.Read(ref reader, typeToConvert, options); } } public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { - if (value.HasValue) writer.WriteStringValue(value.Value.ToZoomFormat()); - else writer.WriteNullValue(); + if (!value.HasValue) writer.WriteNullValue(); + else _dateTimeConverter.Write(writer, value.Value, options); } } } diff --git a/Source/ZoomNet/Json/NullableHexColorConverter.cs b/Source/ZoomNet/Json/NullableHexColorConverter.cs index 8c2f3c02..73308e09 100644 --- a/Source/ZoomNet/Json/NullableHexColorConverter.cs +++ b/Source/ZoomNet/Json/NullableHexColorConverter.cs @@ -11,35 +11,37 @@ namespace ZoomNet.Json; /// public class NullableHexColorConverter : JsonConverter { - /// + /// public override bool CanConvert(Type typeToConvert) { return typeToConvert == typeof(Color?); } - /// + /// public override Color? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var str = reader.GetString(); - if (string.IsNullOrEmpty(str)) - return null; - str = str.Replace("#", string.Empty); - if (str.Length == 6) - str = $"FF{str}"; - return int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argB) - ? Color.FromArgb(argB) - : null; + switch (reader.TokenType) + { + case JsonTokenType.None: + case JsonTokenType.Null: + case JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()): + return null; + + default: + { + var str = reader.GetString().Replace("#", string.Empty); + if (str.Length == 6) str = $"FF{str}"; + return int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argB) + ? Color.FromArgb(argB) + : null; + } + } } - /// + /// public override void Write(Utf8JsonWriter writer, Color? value, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStringValue($"#{value.Value.R:X2}{value.Value.G:X2}{value.Value.B:X2}"); + if (!value.HasValue) writer.WriteNullValue(); + else writer.WriteStringValue($"#{value.Value.R:X2}{value.Value.G:X2}{value.Value.B:X2}"); } } diff --git a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs index 869f1cc3..ff26e552 100644 --- a/Source/ZoomNet/Json/ParticipantDeviceConverter.cs +++ b/Source/ZoomNet/Json/ParticipantDeviceConverter.cs @@ -28,7 +28,7 @@ public override ParticipantDevice[] Read(ref Utf8JsonReader reader, Type typeToC return items; default: - throw new Exception("Unable to convert to ParticipantDevice"); + throw new JsonException("Unable to convert to ParticipantDevice"); } } diff --git a/Source/ZoomNet/Json/WebhookEventConverter.cs b/Source/ZoomNet/Json/WebhookEventConverter.cs index 9a1f5dff..a881c475 100644 --- a/Source/ZoomNet/Json/WebhookEventConverter.cs +++ b/Source/ZoomNet/Json/WebhookEventConverter.cs @@ -280,7 +280,7 @@ public override Event Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe webHookEvent = endpointUrlValidationEvent; break; default: - throw new Exception($"{eventType} is an unknown event type"); + throw new JsonException($"{eventType} is an unknown event type"); } webHookEvent.EventType = eventType; @@ -306,7 +306,7 @@ private static KeyValuePair ConvertJsonPropertyToKeyValuePair(Js if (value.TryGetInt64(out var longValue)) return new KeyValuePair(key, longValue); if (value.TryGetInt32(out var intValue)) return new KeyValuePair(key, intValue); if (value.TryGetInt16(out var shortValue)) return new KeyValuePair(key, shortValue); - throw new Exception($"Property {key} appears to contain a numerical value but we are unable to determine to exact type"); + throw new JsonException($"Property {key} appears to contain a numerical value but we are unable to determine to exact type"); default: return new KeyValuePair(key, value.GetRawText()); } } diff --git a/Source/ZoomNet/Json/WebinarConverter.cs b/Source/ZoomNet/Json/WebinarConverter.cs index bf84d616..5ae880e2 100644 --- a/Source/ZoomNet/Json/WebinarConverter.cs +++ b/Source/ZoomNet/Json/WebinarConverter.cs @@ -25,7 +25,7 @@ public override Webinar Read(ref Utf8JsonReader reader, Type typeToConvert, Json case WebinarType.RecurringNoFixedTime: return rootElement.ToObject(options); default: - throw new Exception($"{webinarType} is an unknown webinar type"); + throw new JsonException($"{webinarType} is an unknown webinar type"); } } } diff --git a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs index 418aaf6d..42f4c6df 100644 --- a/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs +++ b/Source/ZoomNet/Json/ZoomNetJsonSerializerContext.cs @@ -58,7 +58,10 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection), TypeInfoPropertyName = "ChatbotMessageChatbotSection")] [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannel))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelSettings))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelType))] @@ -152,6 +155,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneUser))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType))] [JsonSerializable(typeof(ZoomNet.Models.Poll))] [JsonSerializable(typeof(ZoomNet.Models.PollAnswer))] @@ -208,6 +212,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails))] [JsonSerializable(typeof(ZoomNet.Models.SecuritySettings))] [JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail))] + [JsonSerializable(typeof(ZoomNet.Models.Site))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachment))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType))] [JsonSerializable(typeof(ZoomNet.Models.SmsDirection))] @@ -354,7 +359,10 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotSection[]), TypeInfoPropertyName = "ChatbotMessageChatbotSectionArray")] [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessageInformation[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannel[]))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMember[]))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions[]))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelSettings[]))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelType[]))] @@ -448,6 +456,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.PhoneCallUserStatus[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneNumber[]))] [JsonSerializable(typeof(ZoomNet.Models.PhoneType[]))] + [JsonSerializable(typeof(ZoomNet.Models.PhoneUser[]))] [JsonSerializable(typeof(ZoomNet.Models.PmiMeetingPasswordRequirementType[]))] [JsonSerializable(typeof(ZoomNet.Models.Poll[]))] [JsonSerializable(typeof(ZoomNet.Models.PollAnswer[]))] @@ -504,6 +513,7 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.ScreenshareDetails[]))] [JsonSerializable(typeof(ZoomNet.Models.SecuritySettings[]))] [JsonSerializable(typeof(ZoomNet.Models.SharingAndRecordingDetail[]))] + [JsonSerializable(typeof(ZoomNet.Models.Site[]))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachment[]))] [JsonSerializable(typeof(ZoomNet.Models.SmsAttachmentType[]))] [JsonSerializable(typeof(ZoomNet.Models.SmsDirection[]))] @@ -619,6 +629,9 @@ namespace ZoomNet.Json [JsonSerializable(typeof(ZoomNet.Models.CallLogType?))] [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotActionStyle?), TypeInfoPropertyName = "ChatbotMessageChatbotActionStyleNullable")] [JsonSerializable(typeof(ZoomNet.Models.ChatbotMessage.ChatbotDropdownStaticSource?), TypeInfoPropertyName = "ChatbotMessageChatbotDropdownStaticSourceNullable")] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelAddMemberPermissions?))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelMentionAllPermissions?))] + [JsonSerializable(typeof(ZoomNet.Models.ChatChannelPostingPermissions?))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelRole?))] [JsonSerializable(typeof(ZoomNet.Models.ChatChannelType?))] [JsonSerializable(typeof(ZoomNet.Models.ChatMentionType?))] diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs index 209358d5..ab2a7b32 100644 --- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotFormFields.cs @@ -14,7 +14,7 @@ public class ChatbotFormFields : IChatbotBody, IChatbotSection, IChatbotValidate [JsonPropertyName("items")] public ICollection Items { get; set; } - /// + /// public void Validate(bool enableMarkdownSupport) { foreach (var item in Items) diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs index 50c7150d..676dbe32 100644 --- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotMessageLine.cs @@ -54,7 +54,7 @@ public ChatbotMessageLine(string text) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Link { get; set; } - /// + /// public void Validate(bool enableMarkdownSupport) { if (enableMarkdownSupport && Link != null) diff --git a/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs index c80badc2..2230c100 100644 --- a/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs +++ b/Source/ZoomNet/Models/ChatbotMessage/ChatbotSection.cs @@ -64,7 +64,7 @@ public DateTime TimeStampFromDateTime set => TimeStamp = value.ToUnixTime(Internal.UnixTimePrecision.Milliseconds); } - /// + /// public void Validate(bool enableMarkdownSupport) { foreach (var section in Sections.OfType()) diff --git a/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs index 02b9278d..bd931fd6 100644 --- a/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs +++ b/Source/ZoomNet/Models/ContinuousMeetingChatSettings.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace ZoomNet.Models { /// /// Information about the Enable continuous meeting chat feature. - /// + /// public class ContinuousMeetingChatSettings { /// diff --git a/Source/ZoomNet/Models/PhoneCallUserProfile.cs b/Source/ZoomNet/Models/PhoneCallUserProfile.cs index de9ffd70..095fe729 100644 --- a/Source/ZoomNet/Models/PhoneCallUserProfile.cs +++ b/Source/ZoomNet/Models/PhoneCallUserProfile.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace ZoomNet.Models; diff --git a/Source/ZoomNet/Models/PhoneUser.cs b/Source/ZoomNet/Models/PhoneUser.cs new file mode 100644 index 00000000..58c5b313 --- /dev/null +++ b/Source/ZoomNet/Models/PhoneUser.cs @@ -0,0 +1,81 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models; + +/// +/// Zoom Phone user information. +/// +public class PhoneUser +{ + /// + /// Gets or sets the calling plans. + /// + [JsonPropertyName("calling_plans")] + public CallingPlan[] CallingPlans { get; set; } + + /// + /// Gets or sets the cost center. + /// + [JsonPropertyName("cost_center")] + public string CostCenter { get; set; } + + /// + /// Gets or sets the department of the object. + /// + [JsonPropertyName("department")] + public string Department { get; set; } + + /// + /// Gets or sets the email address. + /// + [JsonPropertyName("email")] + public string Email { get; set; } + + /// + /// Gets or sets the extension ID. + /// + [JsonPropertyName("extension_id")] + public string ExtensionId { get; set; } + + /// + /// Gets or sets the extension number. + /// + [JsonPropertyName("extension_number")] + public int ExtensionNumber { get; set; } + + /// + /// Gets or sets the Zoom user ID. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the Zoom user name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the phone numbers. + /// + [JsonPropertyName("phone_numbers")] + public PhoneCallPhoneNumber[] PhoneNumbers { get; set; } + + /// + /// Gets or sets the Zoom phone user id. + /// + [JsonPropertyName("phone_user_id")] + public string PhoneUserId { get; set; } + + /// + /// Gets or sets a site. + /// + [JsonPropertyName("site")] + public Site Site { get; set; } + + /// + /// Gets or sets the status of the user. + /// + [JsonPropertyName("status")] + public PhoneCallUserStatus Status { get; set; } +} diff --git a/Source/ZoomNet/Models/Site.cs b/Source/ZoomNet/Models/Site.cs new file mode 100644 index 00000000..1375b493 --- /dev/null +++ b/Source/ZoomNet/Models/Site.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace ZoomNet.Models; + +/// +/// Site information to which a user belongs to. +/// +public class Site +{ + /// + /// Gets or sets the Id. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Gets or sets the name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } +} diff --git a/Source/ZoomNet/Resources/CallLogs.cs b/Source/ZoomNet/Resources/CallLogs.cs index 9f2f4441..e9857153 100644 --- a/Source/ZoomNet/Resources/CallLogs.cs +++ b/Source/ZoomNet/Resources/CallLogs.cs @@ -76,7 +76,7 @@ public Task> GetAsync(string userId, Dat /// An array of . /// public Task> GetAsync(DateTime? from = null, DateTime? to = null, CallLogType type = CallLogType.All, CallLogPathType? pathType = null, CallLogTimeType? timeType = CallLogTimeType.StartTime, string siteId = null, bool chargedCallLogs = false, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) - { + { if (recordsPerPage < 1 || recordsPerPage > 300) { throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300"); diff --git a/Source/ZoomNet/Resources/CloudRecordings.cs b/Source/ZoomNet/Resources/CloudRecordings.cs index 5fbe2e5e..66204e7e 100644 --- a/Source/ZoomNet/Resources/CloudRecordings.cs +++ b/Source/ZoomNet/Resources/CloudRecordings.cs @@ -4,10 +4,12 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models; +using ZoomNet.Utilities; namespace ZoomNet.Resources { @@ -356,20 +358,44 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable registran return UpdateRegistrantsStatusAsync(meetingId, registrantIds, "deny", cancellationToken); } - /// - /// Download the recording file. - /// - /// The URL of the recording file to download. - /// Cancellation token. - /// - /// The containing the file. - /// - public Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default) + /// + public async Task DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default) { - return _client + /* + * PLEASE NOTE: + * + * The HttpRequestMessage in this method is dispatched with its completion option set to "ResponseHeadersRead". + * This ensures the content of the response is streamed rather than buffered in memory. + * This is important in cases where the downloaded file is quite large. + * In this scenario, we don't want the entirety of the file to be buffered in a MemoryStream because + * it could lead to "out of memory" exceptions if the file is large enough. + * See https://github.com/Jericho/ZoomNet/pull/342 for a discussion on this topic. + * + * Forthermore, as of this writing, the FluentHttp library does not allow us to stream the content of responses + * which means that the code in this method cannot be simplified like so: + return _client .GetAsync(downloadUrl) .WithCancellationToken(cancellationToken) .AsStream(); + * + * The downside of not using the FluentHttp library to dispatch the request is that we lose automatic retries, + * error handling, logging, etc. + */ + + using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl)) + { + var tokenHandler = _client.Filters.OfType().SingleOrDefault(); + if (tokenHandler != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token); + } + + var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStreamAsync(); + } } private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable registrantIds, string status, CancellationToken cancellationToken = default) diff --git a/Source/ZoomNet/Resources/IPhone.cs b/Source/ZoomNet/Resources/IPhone.cs index fac8a3f8..14702d94 100644 --- a/Source/ZoomNet/Resources/IPhone.cs +++ b/Source/ZoomNet/Resources/IPhone.cs @@ -58,6 +58,36 @@ Task GetRecordingTranscriptAsync( Task GetPhoneCallUserProfileAsync( string userId, CancellationToken cancellationToken = default); + /// + /// Retrieves a list of all of an account's users who are assigned a Zoom Phone license. + /// + /// The number of records returned from a single API call. Default is 30. + /// + /// The next page token paginates through a large set of results. + /// A next page token is returned whenever the set of available results exceeds the current page size. + /// The expiration period for this token is 15 minutes. + /// + /// The unique identifier of the site. + /// The type of calling plan. + /// The status of the Zoom Phone user. + /// The department where the user belongs. + /// The cost center where the user belongs. + /// The partial string of user's name, extension number, or phone number. + /// A cancellation token that can be used to cancel the asynchronous operation. + /// + /// A task representing the asynchronous operation. The task result contains an array of Zoom Phone users in type of . + /// + Task> ListPhoneUsersAsync( + int pageSize = 30, + string nextPageToken = null, + string siteId = null, + int? callingType = null, + PhoneCallUserStatus? status = null, + string department = null, + string costCenter = null, + string keyword = null, + CancellationToken cancellationToken = default); + #endregion } } diff --git a/Source/ZoomNet/Resources/Phone.cs b/Source/ZoomNet/Resources/Phone.cs index fa9e3ea8..f5fdade5 100644 --- a/Source/ZoomNet/Resources/Phone.cs +++ b/Source/ZoomNet/Resources/Phone.cs @@ -1,11 +1,12 @@ using Pathoschild.Http.Client; +using System; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models; namespace ZoomNet.Resources { - /// + /// public class Phone : IPhone { #region private fields @@ -29,7 +30,7 @@ internal Phone(IClient client) #region Recordings endpoints - /// + /// public Task GetRecordingAsync( string callId, CancellationToken cancellationToken = default) { @@ -39,7 +40,7 @@ public Task GetRecordingAsync( .AsObject(); } - /// + /// public Task GetRecordingTranscriptAsync( string recordingId, CancellationToken cancellationToken = default) { @@ -53,7 +54,7 @@ public Task GetRecordingTranscriptAsync( #region Users Endpoints - /// + /// public Task GetPhoneCallUserProfileAsync(string userId, CancellationToken cancellationToken = default) { return _client @@ -62,6 +63,37 @@ public Task GetPhoneCallUserProfileAsync(string userId, Ca .AsObject(); } + /// + public Task> ListPhoneUsersAsync( + int pageSize = 30, + string nextPageToken = null, + string siteId = null, + int? callingType = null, + PhoneCallUserStatus? status = null, + string department = null, + string costCenter = null, + string keyword = null, + CancellationToken cancellationToken = default) + { + if (pageSize < 1 || pageSize > 100) + { + throw new ArgumentOutOfRangeException(nameof(pageSize), "Records per page must be between 1 and 100"); + } + + return _client + .GetAsync($"phone/users") + .WithArgument("page_size", pageSize) + .WithArgument("next_page_token", nextPageToken) + .WithArgument("site_id", siteId) + .WithArgument("calling_type", callingType) + .WithArgument("status", status) + .WithArgument("department", department) + .WithArgument("cost_center", costCenter) + .WithArgument("keyword", keyword) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponseWithToken("users"); + } + #endregion } } diff --git a/Source/ZoomNet/Resources/Sms.cs b/Source/ZoomNet/Resources/Sms.cs index d3600ed8..19e4a18c 100644 --- a/Source/ZoomNet/Resources/Sms.cs +++ b/Source/ZoomNet/Resources/Sms.cs @@ -6,7 +6,7 @@ namespace ZoomNet.Resources { - /// + /// public class Sms : ISms { private readonly IClient _client; @@ -20,7 +20,7 @@ internal Sms(IClient client) _client = client; } - /// + /// public Task> GetSmsSessionDetailsAsync( string sessionId, DateTime? from, DateTime? to, bool? orderAscending = true, int recordsPerPage = 30, string pagingToken = null, CancellationToken cancellationToken = default) {