diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/CHANGELOG.md b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/CHANGELOG.md index c38c3004601c..2a510514062d 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/CHANGELOG.md +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/CHANGELOG.md @@ -47,6 +47,7 @@ - Optional properties `GetAllFeedbackOptions.Filter`, `GetAnomalyDimensionValuesOptions.DimensionToFilter`, and `FeedbackDimensionFilter.DimensionFilter` must now be manually added with setters to be used. - Moved property `DataFeed.SourceType` to `DataFeedSource.DataSourceType`. - In `MetricsAdvisorKeyCredential`, merged `UpdateSubscriptionKey` and `UpdateApiKey` into a single method, `Update`, to make it an atomic operation. +- The class `NotificationHook` is now abstract. ## 1.0.0-beta.4 (2021-06-07) diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/api/Azure.AI.MetricsAdvisor.netstandard2.0.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/api/Azure.AI.MetricsAdvisor.netstandard2.0.cs index 2c9d126869b3..945a134d549d 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/api/Azure.AI.MetricsAdvisor.netstandard2.0.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/api/Azure.AI.MetricsAdvisor.netstandard2.0.cs @@ -471,7 +471,7 @@ public MySqlDataFeedSource(string connectionString, string query) { } public string Query { get { throw null; } set { } } public void UpdateConnectionString(string connectionString) { } } - public partial class NotificationHook + public abstract partial class NotificationHook { internal NotificationHook() { } public System.Collections.Generic.IReadOnlyList AdministratorEmails { get { throw null; } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Generated/Models/NotificationHook.Serialization.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Generated/Models/NotificationHook.Serialization.cs index 9c09e9986d59..691c51a3db22 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Generated/Models/NotificationHook.Serialization.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Generated/Models/NotificationHook.Serialization.cs @@ -5,9 +5,8 @@ #nullable disable -using System.Collections.Generic; +using System; using System.Text.Json; -using Azure.AI.MetricsAdvisor.Models; using Azure.Core; namespace Azure.AI.MetricsAdvisor.Administration @@ -43,67 +42,5 @@ void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) } writer.WriteEndObject(); } - - internal static NotificationHook DeserializeNotificationHook(JsonElement element) - { - if (element.TryGetProperty("hookType", out JsonElement discriminator)) - { - switch (discriminator.GetString()) - { - case "Email": return EmailNotificationHook.DeserializeEmailNotificationHook(element); - case "Webhook": return WebNotificationHook.DeserializeWebNotificationHook(element); - } - } - HookType hookType = default; - Optional hookId = default; - string hookName = default; - Optional description = default; - Optional externalLink = default; - Optional> admins = default; - foreach (var property in element.EnumerateObject()) - { - if (property.NameEquals("hookType")) - { - hookType = new HookType(property.Value.GetString()); - continue; - } - if (property.NameEquals("hookId")) - { - hookId = property.Value.GetString(); - continue; - } - if (property.NameEquals("hookName")) - { - hookName = property.Value.GetString(); - continue; - } - if (property.NameEquals("description")) - { - description = property.Value.GetString(); - continue; - } - if (property.NameEquals("externalLink")) - { - externalLink = property.Value.GetString(); - continue; - } - if (property.NameEquals("admins")) - { - if (property.Value.ValueKind == JsonValueKind.Null) - { - property.ThrowNonNullablePropertyIsNull(); - continue; - } - List array = new List(); - foreach (var item in property.Value.EnumerateArray()) - { - array.Add(item.GetString()); - } - admins = array; - continue; - } - } - return new NotificationHook(hookType, hookId.Value, hookName, description.Value, externalLink.Value, Optional.ToList(admins)); - } } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Models/NotificationHook/NotificationHook.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Models/NotificationHook/NotificationHook.cs index 655a6a21fc6b..5a355158970a 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Models/NotificationHook/NotificationHook.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/src/Models/NotificationHook/NotificationHook.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using Azure.AI.MetricsAdvisor.Models; using Azure.Core; @@ -12,7 +13,7 @@ namespace Azure.AI.MetricsAdvisor.Administration /// An alert notification to be triggered after an anomaly is detected by Metrics Advisor. /// [CodeGenModel("HookInfo")] - public partial class NotificationHook + public abstract partial class NotificationHook { internal NotificationHook(string name) { @@ -85,9 +86,10 @@ internal static HookInfoPatch GetPatchModel(NotificationHook hook) Headers = h.Headers } }, - _ => throw new InvalidOperationException("Unknown hook type.") + _ => new HookInfoPatch() }; + patch.HookType = hook.HookType; patch.HookName = hook.Name; patch.Description = hook.Description; patch.ExternalLink = hook.ExternalUri?.AbsoluteUri; @@ -95,5 +97,77 @@ internal static HookInfoPatch GetPatchModel(NotificationHook hook) return patch; } + + internal static NotificationHook DeserializeNotificationHook(JsonElement element) + { + if (element.TryGetProperty("hookType", out JsonElement discriminator)) + { + switch (discriminator.GetString()) + { + case "Email": + return EmailNotificationHook.DeserializeEmailNotificationHook(element); + case "Webhook": + return WebNotificationHook.DeserializeWebNotificationHook(element); + } + } + HookType hookType = default; + Optional hookId = default; + string hookName = default; + Optional description = default; + Optional externalLink = default; + Optional> admins = default; + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("hookType")) + { + hookType = new HookType(property.Value.GetString()); + continue; + } + if (property.NameEquals("hookId")) + { + hookId = property.Value.GetString(); + continue; + } + if (property.NameEquals("hookName")) + { + hookName = property.Value.GetString(); + continue; + } + if (property.NameEquals("description")) + { + description = property.Value.GetString(); + continue; + } + if (property.NameEquals("externalLink")) + { + externalLink = property.Value.GetString(); + continue; + } + if (property.NameEquals("admins")) + { + if (property.Value.ValueKind == JsonValueKind.Null) + { + property.ThrowNonNullablePropertyIsNull(); + continue; + } + List array = new List(); + foreach (var item in property.Value.EnumerateArray()) + { + array.Add(item.GetString()); + } + admins = array; + continue; + } + } + return new UnknownNotificationHook(hookType, hookId.Value, hookName, description.Value, externalLink.Value, Optional.ToList(admins)); + } + + private class UnknownNotificationHook : NotificationHook + { + public UnknownNotificationHook(HookType hookType, string id, string name, string description, string internalExternalLink, IReadOnlyList administrators) + : base(hookType, id, name, description, internalExternalLink, administrators) + { + } + } } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MetricsAdvisorAdministrationClient/NotificationHookTests.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MetricsAdvisorAdministrationClient/NotificationHookTests.cs index 83270c3c13d0..81c65ad34da5 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MetricsAdvisorAdministrationClient/NotificationHookTests.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MetricsAdvisorAdministrationClient/NotificationHookTests.cs @@ -27,7 +27,6 @@ public void CreateHookValidatesArguments() var name = "hookName"; var endpoint = new Uri("http://fakeendpoint.com"); - var genericHook = new NotificationHook(name); var emailHook = new EmailNotificationHook(name) { Name = null, EmailsToAlert = { "fake@email.com" } }; var webHook = new WebNotificationHook(name, endpoint) { Name = null }; @@ -57,9 +56,6 @@ public void CreateHookValidatesArguments() webHook.Endpoint = null; Assert.That(() => adminClient.CreateHookAsync(webHook), Throws.InstanceOf()); Assert.That(() => adminClient.CreateHook(webHook), Throws.InstanceOf()); - - Assert.That(() => adminClient.CreateHookAsync(genericHook), Throws.InstanceOf()); - Assert.That(() => adminClient.CreateHook(genericHook), Throws.InstanceOf()); } [Test] diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MockClientTestBase.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MockClientTestBase.cs index f2e76a366257..b3b4d195c8d2 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MockClientTestBase.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/MockClientTestBase.cs @@ -4,9 +4,13 @@ using System; using System.IO; using System.Text; +using System.Threading; using Azure.AI.MetricsAdvisor.Administration; +using Azure.Core; using Azure.Core.TestFramework; using Newtonsoft.Json; +using NUnit.Framework; +using NUnit.Framework.Constraints; namespace Azure.AI.MetricsAdvisor.Tests { @@ -75,5 +79,20 @@ public Stream CreateIncidentJsonStream(double valueOfRootNode = default, double? return new MemoryStream(Encoding.UTF8.GetBytes(jsonStr)); } + + public string ReadContent(Request request) + { + using MemoryStream stream = new MemoryStream(); + request.Content.WriteTo(stream, CancellationToken.None); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + + public SubstringConstraint ContainsJsonString(string propertyName, string propertyValue) => + Contains.Substring($"\"{propertyName}\":\"{propertyValue}\""); + + // Currently only supports a single-element array. + public SubstringConstraint ContainsJsonStringArray(string propertyName, string elementValue) => + Contains.Substring($"\"{propertyName}\":[\"{elementValue}\"]"); } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataFeedSourcesTests.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataFeedSourcesTests.cs index 32cf8cfeda67..d74938e33f1c 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataFeedSourcesTests.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataFeedSourcesTests.cs @@ -2,10 +2,7 @@ // Licensed under the MIT License. using System; -using System.IO; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Azure.AI.MetricsAdvisor.Administration; using Azure.AI.MetricsAdvisor.Models; @@ -93,9 +90,8 @@ public async Task DataFeedSourceSendsSecretDuringCreation(DataFeedSource dataSou MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - string expectedSubstring = $"\"{secretPropertyName}\":\"secret\""; - Assert.That(content, Contains.Substring(expectedSubstring)); + Assert.That(content, ContainsJsonString(secretPropertyName, "secret")); } [Test] @@ -125,7 +121,7 @@ public async Task DataFeedSourceSendsSecretDuringUpdate(DataFeedSource dataSourc MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - Assert.That(content, Contains.Substring($"\"{secretPropertyName}\":\"new_secret\"")); + Assert.That(content, ContainsJsonString(secretPropertyName, "new_secret")); } private void UpdateSecret(DataFeedSource dataSource, string secretPropertyName) @@ -176,13 +172,5 @@ private void UpdateSecret(DataFeedSource dataSource, string secretPropertyName) throw new ArgumentOutOfRangeException($"Unknown data source type: {dataSource.GetType()}"); }; } - - private string ReadContent(Request request) - { - using MemoryStream stream = new MemoryStream(); - request.Content.WriteTo(stream, CancellationToken.None); - - return Encoding.UTF8.GetString(stream.ToArray()); - } } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataSourceCredentialEntitiesTests.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataSourceCredentialEntitiesTests.cs index eebdc6fcc312..97335efca18d 100644 --- a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataSourceCredentialEntitiesTests.cs +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/DataSourceCredentialEntitiesTests.cs @@ -2,10 +2,7 @@ // Licensed under the MIT License. using System; -using System.IO; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Azure.AI.MetricsAdvisor.Administration; using Azure.AI.MetricsAdvisor.Models; @@ -83,7 +80,7 @@ public async Task DataLakeSharedKeyCredentialEntitySendsSecretDuringUpdate() MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - Assert.That(content, Contains.Substring("\"accountKey\":\"secret\"")); + Assert.That(content, ContainsJsonString("accountKey", "secret")); } [Test] @@ -107,7 +104,7 @@ public async Task ServicePrincipalCredentialEntitySendsSecretDuringUpdate() MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - Assert.That(content, Contains.Substring("\"clientSecret\":\"secret\"")); + Assert.That(content, ContainsJsonString("clientSecret", "secret")); } [Test] @@ -131,7 +128,7 @@ public async Task ServicePrincipalInKeyVaultCredentialEntitySendsSecretDuringUpd MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - Assert.That(content, Contains.Substring("\"keyVaultClientSecret\":\"secret\"")); + Assert.That(content, ContainsJsonString("keyVaultClientSecret", "secret")); } [Test] @@ -155,15 +152,7 @@ public async Task SqlConnectionStringCredentialEntitySendsSecretDuringUpdate() MockRequest request = mockTransport.Requests.First(); string content = ReadContent(request); - Assert.That(content, Contains.Substring("\"connectionString\":\"secret\"")); - } - - private string ReadContent(Request request) - { - using MemoryStream stream = new MemoryStream(); - request.Content.WriteTo(stream, CancellationToken.None); - - return Encoding.UTF8.GetString(stream.ToArray()); + Assert.That(content, ContainsJsonString("connectionString", "secret")); } } } diff --git a/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/NotificationHookModelTests.cs b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/NotificationHookModelTests.cs new file mode 100644 index 000000000000..d3af120cee70 --- /dev/null +++ b/sdk/metricsadvisor/Azure.AI.MetricsAdvisor/tests/Models/NotificationHookModelTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Azure.AI.MetricsAdvisor.Administration; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.AI.MetricsAdvisor.Tests +{ + public class NotificationHookModelTests : MockClientTestBase + { + public NotificationHookModelTests(bool isAsync) : base(isAsync) + { + } + + private string UnknownHookContent => $@" + {{ + ""hookId"": ""{FakeGuid}"", + ""hookName"": ""unknownHookName"", + ""hookType"": ""unknownType"", + ""externalLink"": ""https://fakeuri.com/"", + ""description"": ""unknown hook description"", + ""admins"": [ + ""foo@contoso.com"" + ] + }} + "; + + [Test] + public async Task NotificationHookGetsUnknownHook() + { + MockResponse getResponse = new MockResponse(200); + getResponse.SetContent(UnknownHookContent); + + MetricsAdvisorAdministrationClient adminClient = CreateInstrumentedAdministrationClient(getResponse); + NotificationHook hook = await adminClient.GetHookAsync(FakeGuid); + + Assert.That(hook.Id, Is.EqualTo(FakeGuid)); + Assert.That(hook.Name, Is.EqualTo("unknownHookName")); + Assert.That(hook.ExternalUri.AbsoluteUri, Is.EqualTo("https://fakeuri.com/")); + Assert.That(hook.Description, Is.EqualTo("unknown hook description")); + Assert.That(hook.AdministratorEmails.Single(), Is.EqualTo("foo@contoso.com")); + } + + [Test] + public async Task NotificationHookUpdatesUnknownHook() + { + MockResponse getResponse = new MockResponse(200); + getResponse.SetContent(UnknownHookContent); + + MockResponse updateResponse = new MockResponse(200); + updateResponse.SetContent(UnknownHookContent); + + MockTransport mockTransport = new MockTransport(getResponse, updateResponse); + MetricsAdvisorAdministrationClient adminClient = CreateInstrumentedAdministrationClient(mockTransport); + NotificationHook hook = await adminClient.GetHookAsync(FakeGuid); + + hook.Name = "newHookName"; + hook.ExternalUri = new Uri("https://newfakeuri.com/"); + hook.Description = "new description"; + + await adminClient.UpdateHookAsync(hook); + + MockRequest request = mockTransport.Requests.Last(); + string content = ReadContent(request); + + Assert.That(request.Uri.Path, Contains.Substring(FakeGuid)); + Assert.That(content, ContainsJsonString("hookName", "newHookName")); + Assert.That(content, ContainsJsonString("hookType", "unknownType")); + Assert.That(content, ContainsJsonString("externalLink", "https://newfakeuri.com/")); + Assert.That(content, ContainsJsonString("description", "new description")); + Assert.That(content, ContainsJsonStringArray("admins", "foo@contoso.com")); + } + } +}