|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.IO; |
| 6 | +using System.Linq; |
| 7 | +using System.Net.Http; |
| 8 | +using System.Net.Http.Json; |
| 9 | +using System.Threading.Tasks; |
| 10 | +using Azure.DataApiBuilder.Config.ObjectModel; |
| 11 | +using Microsoft.ApplicationInsights.Channel; |
| 12 | +using Microsoft.ApplicationInsights.DataContracts; |
| 13 | +using Microsoft.AspNetCore.TestHost; |
| 14 | +using Microsoft.IdentityModel.Tokens; |
| 15 | +using Microsoft.VisualStudio.TestTools.UnitTesting; |
| 16 | +using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationTests; |
| 17 | + |
| 18 | +namespace Azure.DataApiBuilder.Service.Tests.Configuration; |
| 19 | + |
| 20 | +/// <summary> |
| 21 | +/// Contains tests for telemetry functionality. |
| 22 | +/// </summary> |
| 23 | +[TestClass, TestCategory(TestCategory.MSSQL)] |
| 24 | +public class TelemetryTests |
| 25 | +{ |
| 26 | + public TestContext TestContext { get; set; } |
| 27 | + private const string TEST_APP_INSIGHTS_CONN_STRING = "InstrumentationKey=testKey;IngestionEndpoint=https://localhost/;LiveEndpoint=https://localhost/"; |
| 28 | + |
| 29 | + private const string CONFIG_WITH_TELEMETRY = "dab-telemetry-test-config.json"; |
| 30 | + private const string CONFIG_WITHOUT_TELEMETRY = "dab-no-telemetry-test-config.json"; |
| 31 | + private static RuntimeConfig _configuration; |
| 32 | + |
| 33 | + /// <summary> |
| 34 | + /// Creates runtime config file with specified telemetry options. |
| 35 | + /// </summary> |
| 36 | + /// <param name="configFileName">Name of the config file to be created.</param> |
| 37 | + /// <param name="isTelemetryEnabled">Whether telemetry is enabled or not.</param> |
| 38 | + /// <param name="telemetryConnectionString">Telemetry connection string.</param> |
| 39 | + public static void SetUpTelemetryInConfig(string configFileName, bool isTelemetryEnabled, string telemetryConnectionString) |
| 40 | + { |
| 41 | + DataSource dataSource = new(DatabaseType.MSSQL, |
| 42 | + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); |
| 43 | + |
| 44 | + _configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions: new(), restOptions: new()); |
| 45 | + |
| 46 | + TelemetryOptions _testTelemetryOptions = new(new ApplicationInsightsOptions(isTelemetryEnabled, telemetryConnectionString)); |
| 47 | + _configuration = _configuration with { Runtime = _configuration.Runtime with { Telemetry = _testTelemetryOptions } }; |
| 48 | + |
| 49 | + File.WriteAllText(configFileName, _configuration.ToJson()); |
| 50 | + } |
| 51 | + |
| 52 | + /// <summary> |
| 53 | + /// Cleans up the test environment by deleting the runtime config with telemetry options. |
| 54 | + /// </summary> |
| 55 | + [TestCleanup] |
| 56 | + public void CleanUpTelemetryConfig() |
| 57 | + { |
| 58 | + File.Delete(CONFIG_WITH_TELEMETRY); |
| 59 | + File.Delete(CONFIG_WITHOUT_TELEMETRY); |
| 60 | + Startup.AppInsightsOptions = new(); |
| 61 | + Startup.CustomTelemetryChannel = null; |
| 62 | + } |
| 63 | + |
| 64 | + /// <summary> |
| 65 | + /// Test for non-hosted scenario. |
| 66 | + /// Tests that different telemetry items such as Traces or logs, Exceptions and Requests |
| 67 | + /// are correctly sent to application Insights when enabled. |
| 68 | + /// Also asserting on their respective properties. |
| 69 | + /// </summary> |
| 70 | + /// <note> |
| 71 | + /// Commenting Assert on Request Telemetry as it is flaky, sometimes passing sometimes failing. |
| 72 | + /// while on manual testing it is working fine and we see all the request telemetryItems in Application Insights. |
| 73 | + /// Issue to track the fix for this test: https://github.com/Azure/data-api-builder/issues/1734 |
| 74 | + [TestMethod] |
| 75 | + public async Task TestTelemetryItemsAreSentCorrectly_NonHostedScenario() |
| 76 | + { |
| 77 | + SetUpTelemetryInConfig(CONFIG_WITH_TELEMETRY, isTelemetryEnabled: true, TEST_APP_INSIGHTS_CONN_STRING); |
| 78 | + |
| 79 | + string[] args = new[] |
| 80 | + { |
| 81 | + $"--ConfigFileName={CONFIG_WITH_TELEMETRY}" |
| 82 | + }; |
| 83 | + |
| 84 | + ITelemetryChannel telemetryChannel = new CustomTelemetryChannel() |
| 85 | + { |
| 86 | + EndpointAddress = "https://localhost/" |
| 87 | + }; |
| 88 | + Startup.CustomTelemetryChannel = telemetryChannel; |
| 89 | + using (TestServer server = new(Program.CreateWebHostBuilder(args))) |
| 90 | + { |
| 91 | + await TestRestAndGraphQLRequestsOnServerInNonHostedScenario(server); |
| 92 | + } |
| 93 | + |
| 94 | + List<ITelemetry> telemetryItems = ((CustomTelemetryChannel)telemetryChannel).GetTelemetryItems(); |
| 95 | + |
| 96 | + // Assert that we are sending Traces/Requests/Exceptions |
| 97 | + Assert.IsTrue(telemetryItems.Any(item => item is TraceTelemetry)); |
| 98 | + // Assert.IsTrue(telemetryItems.Any(item => item is RequestTelemetry)); |
| 99 | + Assert.IsTrue(telemetryItems.Any(item => item is ExceptionTelemetry)); |
| 100 | + |
| 101 | + // Asserting on Trace telemetry items. |
| 102 | + // Checking for the Logs for the two entities Book and Publisher are correctly sent to Application Insights. |
| 103 | + Assert.IsTrue(telemetryItems.Any(item => |
| 104 | + item is TraceTelemetry |
| 105 | + && ((TraceTelemetry)item).Message.Equals("[Book] REST path: /api/Book") |
| 106 | + && ((TraceTelemetry)item).SeverityLevel == SeverityLevel.Information)); |
| 107 | + |
| 108 | + Assert.IsTrue(telemetryItems.Any(item => |
| 109 | + item is TraceTelemetry |
| 110 | + && ((TraceTelemetry)item).Message.Equals("[Publisher] REST path: /api/Publisher") |
| 111 | + && ((TraceTelemetry)item).SeverityLevel == SeverityLevel.Information)); |
| 112 | + |
| 113 | + // Asserting on Request telemetry items. |
| 114 | + // Assert.AreEqual(2, telemetryItems.Count(item => item is RequestTelemetry)); |
| 115 | + |
| 116 | + // Assert.IsTrue(telemetryItems.Any(item => |
| 117 | + // item is RequestTelemetry |
| 118 | + // && ((RequestTelemetry)item).Name.Equals("POST /graphql") |
| 119 | + // && ((RequestTelemetry)item).ResponseCode.Equals("200") |
| 120 | + // && ((RequestTelemetry)item).Url.PathAndQuery.Equals("/graphql"))); |
| 121 | + |
| 122 | + // Assert.IsTrue(telemetryItems.Any(item => |
| 123 | + // item is RequestTelemetry |
| 124 | + // && ((RequestTelemetry)item).Name.Equals("POST Rest/Insert [route]") |
| 125 | + // && ((RequestTelemetry)item).ResponseCode.Equals("403") |
| 126 | + // && ((RequestTelemetry)item).Url.PathAndQuery.Equals("/api/Publisher/id/1?name=Test"))); |
| 127 | + |
| 128 | + // Assert on the Exceptions telemetry items. |
| 129 | + Assert.AreEqual(1, telemetryItems.Count(item => item is ExceptionTelemetry)); |
| 130 | + Assert.IsTrue(telemetryItems.Any(item => |
| 131 | + item is ExceptionTelemetry |
| 132 | + && ((ExceptionTelemetry)item).Message.Equals("Authorization Failure: Access Not Allowed."))); |
| 133 | + } |
| 134 | + |
| 135 | + /// <summary> |
| 136 | + /// Validates that no telemetry data is sent to CustomTelemetryChannel when |
| 137 | + /// Appsights is disabled OR when no valid connectionstring is provided. |
| 138 | + /// </summary> |
| 139 | + /// <param name="isTelemetryEnabled">Whether telemetry is enabled or not.</param> |
| 140 | + /// <param name="telemetryConnectionString">Telemetry connection string.</param> |
| 141 | + [DataTestMethod] |
| 142 | + [DataRow(false, "", DisplayName = "Configuration without a connection string and with Application Insights disabled.")] |
| 143 | + [DataRow(true, "", DisplayName = "Configuration without a connection string, but with Application Insights enabled.")] |
| 144 | + [DataRow(false, TEST_APP_INSIGHTS_CONN_STRING, DisplayName = "Configuration with a connection string, but with Application Insights disabled.")] |
| 145 | + public async Task TestNoTelemetryItemsSentWhenDisabled_NonHostedScenario(bool isTelemetryEnabled, string telemetryConnectionString) |
| 146 | + { |
| 147 | + SetUpTelemetryInConfig(CONFIG_WITHOUT_TELEMETRY, isTelemetryEnabled, telemetryConnectionString); |
| 148 | + |
| 149 | + string[] args = new[] |
| 150 | + { |
| 151 | + $"--ConfigFileName={CONFIG_WITHOUT_TELEMETRY}" |
| 152 | + }; |
| 153 | + |
| 154 | + ITelemetryChannel telemetryChannel = new CustomTelemetryChannel(); |
| 155 | + Startup.CustomTelemetryChannel = telemetryChannel; |
| 156 | + |
| 157 | + using (TestServer server = new(Program.CreateWebHostBuilder(args))) |
| 158 | + { |
| 159 | + await TestRestAndGraphQLRequestsOnServerInNonHostedScenario(server); |
| 160 | + } |
| 161 | + |
| 162 | + List<ITelemetry> telemetryItems = ((CustomTelemetryChannel)telemetryChannel).GetTelemetryItems(); |
| 163 | + |
| 164 | + // Assert that we are not sending any Traces/Requests/Exceptions to Telemetry |
| 165 | + Assert.IsTrue(telemetryItems.IsNullOrEmpty()); |
| 166 | + } |
| 167 | + |
| 168 | + /// <summary> |
| 169 | + /// This method is just used as helper for other test methods to execute REST and GRaphQL requests |
| 170 | + /// which trigger the logging system to emit logs. |
| 171 | + /// </summary> |
| 172 | + private static async Task TestRestAndGraphQLRequestsOnServerInNonHostedScenario(TestServer server) |
| 173 | + { |
| 174 | + using (HttpClient client = server.CreateClient()) |
| 175 | + { |
| 176 | + string query = @"{ |
| 177 | + book_by_pk(id: 1) { |
| 178 | + id, |
| 179 | + title, |
| 180 | + publisher_id |
| 181 | + } |
| 182 | + }"; |
| 183 | + |
| 184 | + object payload = new { query }; |
| 185 | + |
| 186 | + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") |
| 187 | + { |
| 188 | + Content = JsonContent.Create(payload) |
| 189 | + }; |
| 190 | + |
| 191 | + await client.SendAsync(graphQLRequest); |
| 192 | + |
| 193 | + // POST request on non-accessible entity |
| 194 | + HttpRequestMessage restRequest = new(HttpMethod.Post, "/api/Publisher/id/1?name=Test"); |
| 195 | + await client.SendAsync(restRequest); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + /// <summary> |
| 200 | + /// The class is a custom telemetry channel to capture telemetry items and assert on them. |
| 201 | + /// </summary> |
| 202 | + private class CustomTelemetryChannel : ITelemetryChannel |
| 203 | + { |
| 204 | + private List<ITelemetry> _telemetryItems = new(); |
| 205 | + |
| 206 | + public CustomTelemetryChannel() |
| 207 | + { } |
| 208 | + |
| 209 | + public bool? DeveloperMode { get; set; } |
| 210 | + |
| 211 | + public string EndpointAddress { get; set; } |
| 212 | + |
| 213 | + public void Dispose() |
| 214 | + { } |
| 215 | + |
| 216 | + public void Flush() |
| 217 | + { |
| 218 | + } |
| 219 | + |
| 220 | + public void Send(ITelemetry item) |
| 221 | + { |
| 222 | + _telemetryItems.Add(item); |
| 223 | + } |
| 224 | + |
| 225 | + public List<ITelemetry> GetTelemetryItems() |
| 226 | + { |
| 227 | + return _telemetryItems; |
| 228 | + } |
| 229 | + } |
| 230 | +} |
0 commit comments