diff --git a/ChangesService.Test/ChangesService.Test.csproj b/ChangesService.Test/ChangesService.Test.csproj index 9385eb0d7..7a0b68cd3 100644 --- a/ChangesService.Test/ChangesService.Test.csproj +++ b/ChangesService.Test/ChangesService.Test.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/CodeSnippetsPipeline.Test/CodeSnippetsPipeline.Test.csproj b/CodeSnippetsPipeline.Test/CodeSnippetsPipeline.Test.csproj index 214f1dfca..42b57fd9c 100644 --- a/CodeSnippetsPipeline.Test/CodeSnippetsPipeline.Test.csproj +++ b/CodeSnippetsPipeline.Test/CodeSnippetsPipeline.Test.csproj @@ -9,7 +9,7 @@ - + diff --git a/CodeSnippetsReflection.OData.Test/CodeSnippetsReflection.OData.Test.csproj b/CodeSnippetsReflection.OData.Test/CodeSnippetsReflection.OData.Test.csproj index 58f59f96d..e511f7d43 100644 --- a/CodeSnippetsReflection.OData.Test/CodeSnippetsReflection.OData.Test.csproj +++ b/CodeSnippetsReflection.OData.Test/CodeSnippetsReflection.OData.Test.csproj @@ -12,7 +12,7 @@ all - + all diff --git a/CodeSnippetsReflection.OData/CodeSnippetsReflection.OData.csproj b/CodeSnippetsReflection.OData/CodeSnippetsReflection.OData.csproj index 430ae3f54..cffdd8745 100644 --- a/CodeSnippetsReflection.OData/CodeSnippetsReflection.OData.csproj +++ b/CodeSnippetsReflection.OData/CodeSnippetsReflection.OData.csproj @@ -6,7 +6,7 @@ - + diff --git a/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs b/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs index eb620e3e1..35037f88f 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs @@ -731,6 +731,69 @@ public async Task GeneratesCorrectCollectionTypeAndDerivedInstances() { Assert.Contains("new FileAttachment", result);// Individual items are derived types Assert.Contains("ContentBytes = Convert.FromBase64String(\"SGVsbG8gV29ybGQh\"),", result); } + [Fact] + public async Task GeneratesPropertiesWithSpecialCharacters() { + var sampleJson = @"{ + ""@odata.type"": ""#microsoft.graph.managedIOSLobApp"", + ""displayName"": ""Display Name value"", + ""description"": ""Description value"", + ""publisher"": ""Publisher value"", + ""largeIcon"": { + ""@odata.type"": ""microsoft.graph.mimeContent"", + ""type"": ""Type value"", + ""value"": ""dmFsdWU="" + }, + ""isFeatured"": true, + ""privacyInformationUrl"": ""https://example.com/privacyInformationUrl/"", + ""informationUrl"": ""https://example.com/informationUrl/"", + ""owner"": ""Owner value"", + ""developer"": ""Developer value"", + ""notes"": ""Notes value"", + ""uploadState"": 11, + ""publishingState"": ""processing"", + ""isAssigned"": true, + ""roleScopeTagIds"": [ + ""Role Scope Tag Ids value"" + ], + ""dependentAppCount"": 1, + ""supersedingAppCount"": 3, + ""supersededAppCount"": 2, + ""appAvailability"": ""lineOfBusiness"", + ""version"": ""Version value"", + ""committedContentVersion"": ""Committed Content Version value"", + ""fileName"": ""File Name value"", + ""size"": 4, + ""bundleId"": ""Bundle Id value"", + ""applicableDeviceType"": { + ""@odata.type"": ""microsoft.graph.iosDeviceType"", + ""iPad"": true, + ""iPhoneAndIPod"": true + }, + ""minimumSupportedOperatingSystem"": { + ""@odata.type"": ""microsoft.graph.iosMinimumOperatingSystem"", + ""v8_0"": true, + ""v9_0"": true, + ""v10_0"": true, + ""v11_0"": true, + ""v12_0"": true, + ""v13_0"": true, + ""v14_0"": true, + ""v15_0"": true, + ""v16_0"": true + }, + ""expirationDateTime"": ""2016-12-31T23:57:57.2481234-08:00"", + ""versionNumber"": ""Version Number value"", + ""buildNumber"": ""Build Number value"", + ""identityVersion"": ""Identity Version value"" + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootBetaUrl}/deviceAppManagement/mobileApps/{{mobileAppId}}"){ + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("MinimumSupportedOperatingSystem = new IosMinimumOperatingSystem", result); + Assert.Contains("V80 = true,", result);//Assert that the property was pascal cased + } [Fact] public async Task CorrectlyHandlesTypeFromInUrl() diff --git a/CodeSnippetsReflection.OpenAPI.Test/CodeSnippetsReflection.OpenAPI.Test.csproj b/CodeSnippetsReflection.OpenAPI.Test/CodeSnippetsReflection.OpenAPI.Test.csproj index 433bfeb64..ee520909f 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/CodeSnippetsReflection.OpenAPI.Test.csproj +++ b/CodeSnippetsReflection.OpenAPI.Test/CodeSnippetsReflection.OpenAPI.Test.csproj @@ -11,7 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/CodeSnippetsReflection.OpenAPI.Test/GoGeneratorTests.cs b/CodeSnippetsReflection.OpenAPI.Test/GoGeneratorTests.cs index 93dcbfc5c..bf9788ddf 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/GoGeneratorTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/GoGeneratorTests.cs @@ -97,7 +97,7 @@ public async Task GeneratesTheSnippetHeader() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("graphClient, err := msgraphsdk.NewGraphServiceClientWithCredentials(cred, scopes)", result); + Assert.Contains("graphClient := msgraphsdk.NewGraphServiceClientWithCredentials(cred, scopes)", result); } [Fact] public async Task GeneratesMultipleImportStatements() diff --git a/CodeSnippetsReflection.OpenAPI.Test/GraphCliGeneratorTests.cs b/CodeSnippetsReflection.OpenAPI.Test/GraphCliGeneratorTests.cs index 3a520b209..ace22bd08 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/GraphCliGeneratorTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/GraphCliGeneratorTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using CodeSnippetsReflection.OpenAPI.Test; @@ -408,4 +408,122 @@ public async Task GeneratesEscapedSnippetsForMultilineCommand() // Then Assert.Equal("mgc users create --body '{\\\n \"name\": \"test\"\\\n}'", result); } + + [Fact] + public async Task GeneratesSnippetsContainingOverLoadedBoundFunctionsWithDateParameter() + { + // Given + string url = $"{ServiceRootUrl}/reports/getYammerDeviceUsageUserDetail(date=2018-03-05)"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc reports get-yammer-device-usage-user-detail-with-date get --date {date-id}", result); + } + + [Fact] + public async Task GeneratesSnippetsContainingOverLoadedBoundFunctionsWithDateParameterWithSingleOrDoubleQuotes() + { + // Given + string url = $"{ServiceRootUrl}/reports/getYammerDeviceUsageUserDetail(date='2018-03-05')"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc reports get-yammer-device-usage-user-detail-with-date get --date '{date-id}'", result); + } + + [Fact] + public async Task GeneratesSnippetsContainingOverLoadedBoundFunctionsWithNonDateParameter() + { + // Given + string url = $"{ServiceRootUrl}/drives/driveid/items/driveitemid/delta(token='token')"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc drives items delta-with-token get --token '{token-id}' --drive-id {drive-id} --drive-item-id {driveItem-id}", result); + } + + [Fact] + public async Task GeneratesSnippetsContainingUnBoundedFunctions() + { + // Given + string url = $"{ServiceRootUrl}/identity/identityProviders/availableProviderTypes()"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc identity identity-providers available-provider-types get", result); + } + + [Fact] + public async Task GeneratesSnippetsWithSlashMeEndpoints() + { + // Given + string url = $"{ServiceRootUrl}/me/calendar/events?$filter=startsWith%28subject%2C%27All%27%29"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc users calendar events list --user-id {user-id} --filter \"startsWith(subject,'All')\"", result); + } + [Fact] + public async Task GeneratesSnippetsWithExpandQueryOptions() + { + // Given + string url = $"{ServiceRootUrl}/me/messages/XXXX?$expand=singleValueExtendedProperties%28$filter%3Did%20eq%20%27XXXX%27%29"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc users messages get --user-id {user-id} --message-id {message-id} --expand \"singleValueExtendedProperties(`$filter=id eq 'XXXX')\"", result); + } + [Fact] + public async Task GeneratesSnippetsWithFilterQueryOptions() + { + // Given + string url = $"{ServiceRootUrl}/identityGovernance/accessReviews/definitions?$filter=contains%28scope%2Fmicrosoft.graph.accessReviewQueryScope%2Fquery%2C%20%27.%2Fmembers%27%29"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc identity-governance access-reviews definitions list --filter \"contains(scope/microsoft.graph.accessReviewQueryScope/query, './members')\"", result); + } + + [Fact] + public async Task GeneratesSnippetsForHttpSnippetsWithUrlEncodedValuesForSystemQueryOptionParameters() + { + // Given + string url = $"{ServiceRootUrl}/teams/XXXXXXX/members?$filter=%28microsoft.graph.aadUserConversationMember%2FdisplayName%2520eq%2520%27Harry%2520Johnson%27%2520or%2520microsoft.graph.aadUserConversationMember%2Femail%2520eq%2520%27admin%40M365x987948.OnMicrosoft.com%27%29"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, url); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + + // When + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Then + Assert.Equal("mgc teams members list --team-id {team-id} --filter \"(microsoft.graph.aadUserConversationMember/displayName eq 'Harry Johnson' or microsoft.graph.aadUserConversationMember/email eq 'admin@M365x987948.OnMicrosoft.com')\"", result); + } } diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/CSharpGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/CSharpGenerator.cs index 2e82988e5..7d7ca5998 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/CSharpGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/CSharpGenerator.cs @@ -167,7 +167,7 @@ private static void WriteObjectFromCodeProperty(CodeProperty parentProperty, Cod var isParentArray = parentProperty.PropertyType == PropertyType.Array; var isParentMap = parentProperty.PropertyType == PropertyType.Map; var assignmentSuffix = isParentMap ? string.Empty : ","; // no comma separator values for additionalData/maps - var propertyAssignment = $"{indentManager.GetIndent()}{codeProperty.Name.CleanupSymbolName().ToFirstCharacterUpperCase()} = "; // default assignments to the usual "var x = xyz" + var propertyAssignment = $"{indentManager.GetIndent()}{codeProperty.Name.CleanupSymbolName().ToPascalCase()} = "; // default assignments to the usual "var x = xyz" if (isParentMap) { propertyAssignment = $"{indentManager.GetIndent()}\"{codeProperty.Name}\" , "; // if its in the additionalData assignments happen using string value keys diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs index ba7960948..14b14f7fa 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GoGenerator.cs @@ -184,7 +184,7 @@ private static Boolean searchProperty(CodeProperty property, PropertyType proper private static void writeSnippet(SnippetCodeGraph codeGraph, StringBuilder builder) { - builder.AppendLine($"{clientVarName}, err := msgraphsdk.New{clientVarType}({clientFactoryVariables}){Environment.NewLine}{Environment.NewLine}"); + builder.AppendLine($"{clientVarName} := msgraphsdk.New{clientVarType}({clientFactoryVariables}){Environment.NewLine}{Environment.NewLine}"); writeHeadersAndOptions(codeGraph, builder); WriteBody(codeGraph, builder); builder.AppendLine(""); diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GraphCliGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GraphCliGenerator.cs index b1d757499..376180d91 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GraphCliGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/GraphCliGenerator.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; +using System.Web; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -13,6 +14,11 @@ public partial class GraphCliGenerator : ILanguageGenerator + /// Checks for segments that have unbound functions + /// Example "identity-providers available-provider-types() get" + /// will be reconstructed to identity-providers available-provider-types get + /// + /// + private static void FetchUnBoundFunctions(List commandSegments) + { + int unboundedFunctionIndex = commandSegments.FindIndex(static u => unBoundFunctionRegex.IsMatch(u)); + if (unboundedFunctionIndex != -1) + { + var segment = commandSegments[unboundedFunctionIndex]; + commandSegments[unboundedFunctionIndex] = segment.Replace("(", "").Replace(")", ""); + } + } + + /// + /// Checks for segments that have overloaded bound functions with date parameter + /// Example of such a segment would be: getYammerDeviceUsageUserDetail(date=2018-03-05). + /// ProcessOverloadedBoundFunction is called to reconstruct the segment to the expected command segment. + /// + /// + /// + private static void FetchOverLoadedBoundFunctions(List commandSegments, string operationName, SnippetModel snippetModel) + { + int boundedFunctionIndex = commandSegments.FindIndex(static u => overloadedBoundedFunctionWithDateRegex.IsMatch(u) + || overloadedBoundedFunctionWithNoneDateRegex.IsMatch(u)); + + if (boundedFunctionIndex != -1) + { + int operationIndex = commandSegments.FindIndex(o => operationName.Equals(o, StringComparison.OrdinalIgnoreCase)); + var (updatedSegment, updatedOperation) = ProcessOverloadedBoundFunctions(commandSegments[boundedFunctionIndex], operationName, snippetModel); + commandSegments[boundedFunctionIndex] = updatedSegment; + commandSegments[operationIndex] = updatedOperation; + } + } + + /// + /// Reconstructs segments with overloaded bound functions to the expected command segments. + /// For example; "get-yammer-device-usage-user-detail(date={date})" get will be reconstructed to + /// "get-yammer-device-usage-user-detail-with-date get --date {date_id}" as expected by cli for it + /// to execute successfully. + /// + /// + /// + /// + private static (string,string) ProcessOverloadedBoundFunctions(string segment, string operation, SnippetModel snippetModel) + { + var functionItems = segment.Split("("); + var functionParams = functionItems[1]; + var functionName = functionItems[0]; + var parameter = functionParams.Split("=")[0]; + var updatedSegment = $"{functionName}-with-{parameter}"; + return apiPathWithSingleOrDoubleQuotesOnFunctions.IsMatch(snippetModel.Path) ? (updatedSegment,$"{operation} --{parameter}"+" '{"+parameter+"-id}'") + : (updatedSegment, $"{operation} --{parameter}" + " {" + parameter + "-id}"); + } + + /// + /// Replaces /me endpoints with users and appends --user-id parameter to the command + /// See issue: https://github.com/microsoftgraph/msgraph-cli/issues/278 + /// + /// + /// + private static void ProcessMeSegments(List commandSegments, string operationName) + { + if (commandSegments[1].Equals("me", StringComparison.OrdinalIgnoreCase)) + { + commandSegments[1] = "users"; + int operationIndex = commandSegments.FindIndex(o => o.Equals(operationName, StringComparison.OrdinalIgnoreCase)); + commandSegments[operationIndex] = $"{operationName} --user-id {{user-id}}"; + } } private static IDictionary ProcessHeaderParameters([NotNull] in SnippetModel snippetModel) @@ -138,16 +222,30 @@ private static IDictionary ProcessQueryParameters([NotNull] in S IDictionary splitQueryString = new Dictionary(); if (!string.IsNullOrWhiteSpace(snippetModel.QueryString)) { - splitQueryString = snippetModel.QueryString - .Remove(0, 1) - .Split('&') - .Select(q => - { - var x = q.Split('='); - return x.Length > 1 ? (x[0], x[1]) : (x[0], string.Empty); - }) - .Where(t => !string.IsNullOrWhiteSpace(t.Item2)) - .ToDictionary(t => t.Item1, t => t.Item2); + if (systemQueryOptionRegex.IsMatch(snippetModel.QueryString)) + { + string pattern = "\\?\\$\\w*="; + string[] splittedQueryString = Regex.Split(snippetModel.QueryString, pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(5)); + string queryOptionFunction = HttpUtility.UrlDecode(splittedQueryString[1]); + var match = Regex.Match(snippetModel.QueryString, pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(5)); + string queryOption = match.Groups[0].Value.Replace("?", string.Empty, StringComparison.OrdinalIgnoreCase).Replace("=", string.Empty, StringComparison.OrdinalIgnoreCase); + queryOptionFunction = queryOptionFunction.Replace("$", "`$", StringComparison.OrdinalIgnoreCase); + queryOptionFunction = queryOptionFunction.Replace(queryOptionFunction, "\"" + queryOptionFunction + "\"", StringComparison.Ordinal); + splitQueryString.Add(queryOption, queryOptionFunction); + } + else + { + splitQueryString = snippetModel.QueryString + .Remove(0, 1) + .Split('&') + .Select(static q => + { + var x = q.Split('='); + return x.Length > 1 ? (x[0], x[1]) : (x[0], string.Empty); + }) + .Where(static t => !string.IsNullOrWhiteSpace(t.Item2)) + .ToDictionary(static t => t.Item1, static t => t.Item2); + } } return splitQueryString; diff --git a/CodeSnippetsReflection.OpenAPI/OpenAPISnippetsGenerator.cs b/CodeSnippetsReflection.OpenAPI/OpenAPISnippetsGenerator.cs index f2c72be5a..ba5125da6 100644 --- a/CodeSnippetsReflection.OpenAPI/OpenAPISnippetsGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/OpenAPISnippetsGenerator.cs @@ -70,7 +70,8 @@ public string ProcessPayloadRequest(HttpRequestMessage requestPayload, string la "go", "powershell", "php", - "python" + "python", + "cli" }; private static ILanguageGenerator GetLanguageGenerator(string language) { return language.ToLowerInvariant() switch { @@ -80,6 +81,7 @@ private static ILanguageGenerator GetLanguageG "powershell" => new PowerShellGenerator(), "php" => new PhpGenerator(), "python" => new PythonGenerator(), + "cli" => new GraphCliGenerator(), _ => throw new ArgumentOutOfRangeException($"Language '{language}' is not supported"), }; } diff --git a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs index 404c0ff38..61aecfdfa 100644 --- a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs +++ b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs @@ -41,13 +41,15 @@ public static string ToFirstCharacterUpperCaseAfterCharacter(this string stringV public static string ToPascalCase(this string str) { + if (string.IsNullOrEmpty(str)) return str; string[] words = str.Split('_'); - string pascalCaseString = string.Join("", words.Select(w => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(w))); + string pascalCaseString = string.Join("", words.Select(ToFirstCharacterUpperCase)); return pascalCaseString; } public static string ToSnakeCase(this string str) { + if (string.IsNullOrEmpty(str)) return str; StringBuilder snakeCaseBuilder = new StringBuilder(); for (int i = 0; i < str.Length; i++) { diff --git a/ExceptionMiddleware/ExceptionMiddleware.Test.csproj b/ExceptionMiddleware/ExceptionMiddleware.Test.csproj index b50c5d89f..9f3146e3a 100644 --- a/ExceptionMiddleware/ExceptionMiddleware.Test.csproj +++ b/ExceptionMiddleware/ExceptionMiddleware.Test.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/FileService.Test/FileService.Test.csproj b/FileService.Test/FileService.Test.csproj index 36858efa7..c67f1823f 100644 --- a/FileService.Test/FileService.Test.csproj +++ b/FileService.Test/FileService.Test.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/GraphWebApi/Controllers/KnownIssuesController.cs b/GraphWebApi/Controllers/KnownIssuesController.cs index 824223a7f..6e90cdd98 100644 --- a/GraphWebApi/Controllers/KnownIssuesController.cs +++ b/GraphWebApi/Controllers/KnownIssuesController.cs @@ -7,7 +7,6 @@ using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; diff --git a/GraphWebApi/Controllers/PermissionsController.cs b/GraphWebApi/Controllers/PermissionsController.cs index 667691f42..4dbf027d6 100644 --- a/GraphWebApi/Controllers/PermissionsController.cs +++ b/GraphWebApi/Controllers/PermissionsController.cs @@ -42,7 +42,7 @@ public PermissionsController(IPermissionsStore permissionsStore, TelemetryClient // Gets the permissions scopes [HttpGet] [Produces("application/json")] - public async Task GetPermissionScopes([FromQuery]ScopeType scopeType = ScopeType.DelegatedWork, + public async Task GetPermissionScopes([FromQuery]ScopeType? scopeType = null, [FromQuery]string requestUrl = null, [FromQuery]string method = null, [FromQuery]string org = null, diff --git a/KnownIssuesService.Test/KnownIssuesService.Test.csproj b/KnownIssuesService.Test/KnownIssuesService.Test.csproj index 0aafa45be..d8e46d1e6 100644 --- a/KnownIssuesService.Test/KnownIssuesService.Test.csproj +++ b/KnownIssuesService.Test/KnownIssuesService.Test.csproj @@ -22,7 +22,7 @@ - + diff --git a/KnownIssuesService.Test/KnownIssuesServiceShould.cs b/KnownIssuesService.Test/KnownIssuesServiceShould.cs index acea5de75..73e34fb80 100644 --- a/KnownIssuesService.Test/KnownIssuesServiceShould.cs +++ b/KnownIssuesService.Test/KnownIssuesServiceShould.cs @@ -78,7 +78,8 @@ public async Task QueryBugs() Link = "/foo/bar", CreatedDateTime = DateTime.Parse("01/06/2022 00:00:00"), LastUpdatedDateTime = DateTime.Parse("01/07/2022 00:00:00"), - SubArea = "Test notifications" + SubArea = "Test notifications", + IsPublicIssue = true }; //Act @@ -99,7 +100,7 @@ public async Task QueryBugs() Assert.Equal(contract.WorkAround, items[1].WorkAround); Assert.Equal(contract.CreatedDateTime, items[1].CreatedDateTime); Assert.Equal(contract.LastUpdatedDateTime, items[1].LastUpdatedDateTime); - Assert.True(items[1].IsUpdated); + Assert.True(items[1].IsDateUpdated); } } } diff --git a/KnownIssuesService.Test/WorkItemsStubData.cs b/KnownIssuesService.Test/WorkItemsStubData.cs index 3779ad38f..9aec0e292 100644 --- a/KnownIssuesService.Test/WorkItemsStubData.cs +++ b/KnownIssuesService.Test/WorkItemsStubData.cs @@ -34,7 +34,8 @@ public static List GetWorkItems() {"System.State","Active"}, {"System.Title","Issue A"}, {"Custom.MSGraphM365Workload","Calendar"}, - {"Custom.Workaround","Test"} + {"Custom.Workaround","Test"}, + {"Custom.PublicIssue", true} } }, new WorkItem(){ Id = 9076 , Url = "https://microsoftgraph.visualstudio.com/_apis/wit/workItems/9076", @@ -45,7 +46,8 @@ public static List GetWorkItems() {"Custom.Workaround","Test"}, {"Custom.APIPathLink", "/foo/bar"}, {"Custom.Dateissuewasraised", DateTime.Parse("01/06/2022 00:00:00")}, - {"Custom.Lastupdate", DateTime.Parse("01/07/2022 00:00:00")} + {"Custom.Lastupdate", DateTime.Parse("01/07/2022 00:00:00")}, + {"Custom.PublicIssue", true} } }, @@ -54,7 +56,8 @@ public static List GetWorkItems() {"System.State","Resolved"}, {"System.Title","Issue K"}, {"Custom.MSGraphM365Workload","Mail"}, - {"Custom.Workaround","Limit number of requests"} + {"Custom.Workaround","Limit number of requests"}, + {"Custom.PublicIssue", true} } }, new WorkItem(){ Id = 9078 , Url = "https://microsoftgraph.visualstudio.com/_apis/wit/workItems/9078", @@ -62,7 +65,8 @@ public static List GetWorkItems() {"System.State","New"}, {"System.Title","Issue F"}, {"Custom.MSGraphM365Workload","Mail"}, - {"Custom.Workaround","Limit number of requests"} + {"Custom.Workaround","Limit number of requests"}, + {"Custom.PublicIssue", false} } }, new WorkItem() @@ -71,7 +75,8 @@ public static List GetWorkItems() {"System.State","New"}, {"System.Title","Issue A"}, {"Custom.MSGraphM365Workload","Calendar"}, - {"Custom.Workaround","Test"} + {"Custom.Workaround","Test"}, + {"Custom.PublicIssue", true} } } }; diff --git a/KnownIssuesService/Models/KnownIssue.cs b/KnownIssuesService/Models/KnownIssue.cs index c25926aeb..68330c60c 100644 --- a/KnownIssuesService/Models/KnownIssue.cs +++ b/KnownIssuesService/Models/KnownIssue.cs @@ -64,8 +64,8 @@ public record KnownIssue /// /// Determines if the Last Update is current compared to the Created date /// - [JsonProperty(nameof(IsUpdated))] - public bool IsUpdated => LastUpdatedDateTime > CreatedDateTime; + [JsonProperty(nameof(IsDateUpdated))] + public bool IsDateUpdated => LastUpdatedDateTime > CreatedDateTime; /// /// Known Issues Status i.e New, Active,Resolved @@ -81,5 +81,13 @@ public string SubArea { get; set; } + + /// + /// Microsoft Graph Issue Visibility + /// + public Boolean IsPublicIssue + { + get; set; + } } } diff --git a/KnownIssuesService/Services/KnownIssuesService.cs b/KnownIssuesService/Services/KnownIssuesService.cs index a95946a68..eb9fe662a 100644 --- a/KnownIssuesService/Services/KnownIssuesService.cs +++ b/KnownIssuesService/Services/KnownIssuesService.cs @@ -172,12 +172,13 @@ public async Task> QueryBugsAsync(string environment, Wiql work Link = x.Fields.TryGetValue("Custom.APIPathLink", out var link) ? link.ToString() : default, CreatedDateTime = x.Fields.TryGetValue("Custom.Dateissuewasraised", out DateTime createdDate) ? createdDate : default, LastUpdatedDateTime = x.Fields.TryGetValue("Custom.Lastupdate", out DateTime changedDate) ? changedDate : default, - SubArea = x.Fields.TryGetValue("Custom.MicrosoftGraphSubarea", out var subArea) ? subArea.ToString() : default + SubArea = x.Fields.TryGetValue("Custom.MicrosoftGraphSubarea", out var subArea) ? subArea.ToString() : default, + IsPublicIssue = x.Fields.TryGetValue("Custom.PublicIssue", out bool publicIssue) ? publicIssue : default }).ToList(); foreach(var knownIssue in _knownIssuesList.ToList()) { - if(knownIssue.State == "New" || knownIssue.State == "Closed") + if(knownIssue.State == "New" || knownIssue.State == "Closed" || !knownIssue.IsPublicIssue) { _knownIssuesList.Remove(knownIssue); } diff --git a/OpenAPIService.Test/OpenAPIService.Test.csproj b/OpenAPIService.Test/OpenAPIService.Test.csproj index d1d28eb46..370a5d22f 100644 --- a/OpenAPIService.Test/OpenAPIService.Test.csproj +++ b/OpenAPIService.Test/OpenAPIService.Test.csproj @@ -26,7 +26,7 @@ all - + diff --git a/OpenAPIService/OpenAPIService.csproj b/OpenAPIService/OpenAPIService.csproj index 64f56c116..af204e887 100644 --- a/OpenAPIService/OpenAPIService.csproj +++ b/OpenAPIService/OpenAPIService.csproj @@ -9,7 +9,7 @@ - + diff --git a/PermissionsService.Test/PermissionsService.Test.csproj b/PermissionsService.Test/PermissionsService.Test.csproj index 40abf80d2..bfea23d2a 100644 --- a/PermissionsService.Test/PermissionsService.Test.csproj +++ b/PermissionsService.Test/PermissionsService.Test.csproj @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all diff --git a/PermissionsService.Test/PermissionsStoreShould.cs b/PermissionsService.Test/PermissionsStoreShould.cs index fc15b716b..a982a70a4 100644 --- a/PermissionsService.Test/PermissionsStoreShould.cs +++ b/PermissionsService.Test/PermissionsStoreShould.cs @@ -363,25 +363,38 @@ public async Task ReturnLocalizedPermissionsDescriptionsForSupportedLanguage() } [Fact] - public async Task ReturnsErrorsForEmptyOrNullRequestUrls() + public async Task ReturnsErrorsForEmptyRequestUrl() { // Act PermissionResult result = await _permissionsStore.GetScopesAsync( - requests: new List() { - new RequestInfo { RequestUrl = "", HttpMethod = "GET" }, - new RequestInfo { RequestUrl = null, HttpMethod = "GET" } } - ); + requests: new List() { + new RequestInfo { RequestUrl = "", HttpMethod = "GET" } }); // Assert Assert.Empty(result.Results); Assert.NotEmpty(result.Errors); - Assert.Equal(2, result.Errors.Count); + Assert.Single(result.Errors); Assert.Collection(result.Errors, item => { Assert.Equal("", item.RequestUrl); Assert.Equal("The request URL cannot be null or empty.", item.Message); - }, + }); + } + + [Fact] + public async Task ReturnsErrorsForNullRequestUrl() + { + // Act + PermissionResult result = + await _permissionsStore.GetScopesAsync( + requests: new List() { + new RequestInfo { RequestUrl = null, HttpMethod = "GET" } }); + // Assert + Assert.Empty(result.Results); + Assert.NotEmpty(result.Errors); + Assert.Single(result.Errors); + Assert.Collection(result.Errors, item => { Assert.Null(item.RequestUrl); diff --git a/PermissionsService/Services/PermissionsStore.cs b/PermissionsService/Services/PermissionsStore.cs index 991034ce2..7266c9bec 100644 --- a/PermissionsService/Services/PermissionsStore.cs +++ b/PermissionsService/Services/PermissionsStore.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------- using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -332,8 +333,10 @@ public async Task GetScopesAsync(List requests = } else { - var scopesByRequestUrl = new Dictionary>(); - foreach (var request in requests) + var scopesByRequestUrl = new ConcurrentDictionary>(); + var uniqueRequests = requests.DistinctBy(static x => $"{x.HttpMethod}{x.RequestUrl}", StringComparer.OrdinalIgnoreCase); + + await Parallel.ForEachAsync(uniqueRequests, async (request, _) => { try { @@ -351,21 +354,20 @@ public async Task GetScopesAsync(List requests = Message = exception.Message }); } - } + }); var allLeastPrivilegeScopes = scopesByRequestUrl.Values - .SelectMany(static x => x).Where(static x => x.IsLeastPrivilege == true).ToList(); + .SelectMany(static x => x).Where(static x => x.IsLeastPrivilege == true) + .DistinctBy(static x => $"{x.ScopeName}{x.ScopeType}", StringComparer.OrdinalIgnoreCase).ToList(); foreach (var scopeSet in scopesByRequestUrl.Values) { - bool foundInOthers = false; var higherPrivilegedScopes = scopeSet.Where(static x => x.IsLeastPrivilege == false); // If any of the higher privilege permissions is a leastPrivilegePermissions somewhere, ignore - if (higherPrivilegedScopes.Any(scope => + bool foundInOthers = higherPrivilegedScopes.Any(scope => allLeastPrivilegeScopes.Any(leastScope => - leastScope.ScopeName.Equals(scope.ScopeName, StringComparison.OrdinalIgnoreCase) && - leastScope.ScopeType == scope.ScopeType))) - foundInOthers = true; + leastScope.ScopeName.Equals(scope.ScopeName, StringComparison.OrdinalIgnoreCase) && + leastScope.ScopeType == scope.ScopeType)); if (!foundInOthers) scopes.AddRange(scopeSet); diff --git a/SamplesService.Test/SamplesService.Test.csproj b/SamplesService.Test/SamplesService.Test.csproj index f8283512e..ba522cbe9 100644 --- a/SamplesService.Test/SamplesService.Test.csproj +++ b/SamplesService.Test/SamplesService.Test.csproj @@ -56,7 +56,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all diff --git a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj index 288ee1302..edccb3645 100644 --- a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj +++ b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj @@ -26,7 +26,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TourStepsService.Test/TourStepsService.Test.csproj b/TourStepsService.Test/TourStepsService.Test.csproj index 7dcd5d45e..6f2dcdde6 100644 --- a/TourStepsService.Test/TourStepsService.Test.csproj +++ b/TourStepsService.Test/TourStepsService.Test.csproj @@ -53,7 +53,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UriMatchService.Test/UriMatchingService.Test.csproj b/UriMatchService.Test/UriMatchingService.Test.csproj index 94c1df97f..b7ea6e033 100644 --- a/UriMatchService.Test/UriMatchingService.Test.csproj +++ b/UriMatchService.Test/UriMatchingService.Test.csproj @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + all diff --git a/UtilityService.Test/UtilityService.Test.csproj b/UtilityService.Test/UtilityService.Test.csproj index 39092ed27..06f455293 100644 --- a/UtilityService.Test/UtilityService.Test.csproj +++ b/UtilityService.Test/UtilityService.Test.csproj @@ -11,7 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/pipelines/snippets.yml b/pipelines/snippets.yml index aa2427903..fbb0ab2d6 100644 --- a/pipelines/snippets.yml +++ b/pipelines/snippets.yml @@ -28,7 +28,7 @@ pool: parameters: - name: snippetLanguages type: object - default: ['C#', 'JavaScript', 'Java', 'Go', 'PowerShell', 'PHP', 'Python'] + default: ['C#', 'CLI', 'Go', 'Java', 'JavaScript', 'PHP', 'PowerShell', 'Python'] # should be ordered alphabetically displayName: 'Languages to generate snippets for' variables: