From 0dfe66665a40995d1800b6085b84e46a879b2039 Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Mon, 8 Oct 2018 15:40:40 -0400 Subject: [PATCH 1/8] devops: fix null reference if no parameter exists --- AzureDevOps.Authentication/Src/Authentication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AzureDevOps.Authentication/Src/Authentication.cs b/AzureDevOps.Authentication/Src/Authentication.cs index 7362811ad..8b62767ed 100644 --- a/AzureDevOps.Authentication/Src/Authentication.cs +++ b/AzureDevOps.Authentication/Src/Authentication.cs @@ -238,7 +238,7 @@ public override async Task DeleteCredentials(TargetUri targetUri) var value = header.Parameter; - if (value.Length >= AuthorizationUriPrefix.Length + AuthorityHostUrlBase.Length + GuidStringLength) + if (value?.Length >= AuthorizationUriPrefix.Length + AuthorityHostUrlBase.Length + GuidStringLength) { // The header parameter will look something like "authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47" // and all we want is the portion after the '=' and before the last '/'. From b1027fb0b2a81d6528aa7bded028f7fcdf37ceab Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Mon, 8 Oct 2018 15:51:00 -0400 Subject: [PATCH 2/8] update version numbers (v1.18.1) --- Cli/Askpass/Properties/AssemblyInfo.cs | 4 ++-- Cli/Manager/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cli/Askpass/Properties/AssemblyInfo.cs b/Cli/Askpass/Properties/AssemblyInfo.cs index 61a36c4b8..e69e61aae 100644 --- a/Cli/Askpass/Properties/AssemblyInfo.cs +++ b/Cli/Askpass/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-1230-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.0.0")] -[assembly: AssemblyFileVersion("1.18.0.0")] +[assembly: AssemblyVersion("1.18.1.0")] +[assembly: AssemblyFileVersion("1.18.1.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed. diff --git a/Cli/Manager/Properties/AssemblyInfo.cs b/Cli/Manager/Properties/AssemblyInfo.cs index c69d64458..fc9944bfc 100644 --- a/Cli/Manager/Properties/AssemblyInfo.cs +++ b/Cli/Manager/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-0415-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.0.0")] -[assembly: AssemblyFileVersion("1.18.0.0")] +[assembly: AssemblyVersion("1.18.1.0")] +[assembly: AssemblyFileVersion("1.18.1.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed. From 81fd82d491c58df3d4883625620754d851cbbb03 Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Tue, 9 Oct 2018 11:24:24 -0400 Subject: [PATCH 3/8] devops: don't null ref when ContentType is null --- Microsoft.Alm.Authentication/Src/Network.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Microsoft.Alm.Authentication/Src/Network.cs b/Microsoft.Alm.Authentication/Src/Network.cs index e439dfb6d..cd7fd2143 100644 --- a/Microsoft.Alm.Authentication/Src/Network.cs +++ b/Microsoft.Alm.Authentication/Src/Network.cs @@ -687,7 +687,7 @@ public async Task SetContent(HttpContent content) if (content is null) throw new ArgumentNullException(nameof(content)); - if (content.Headers.ContentType.MediaType != null + if (content.Headers.ContentType?.MediaType != null && (content.Headers.ContentType.MediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) || content.Headers.ContentType.MediaType.EndsWith("/json", StringComparison.OrdinalIgnoreCase))) { @@ -755,7 +755,7 @@ public async Task SetContent(HttpContent content) lock (_syncpoint) { - _mediaType = content.Headers.ContentType.MediaType; + _mediaType = content.Headers.ContentType?.MediaType; _byteArray = asBytes; } } From 23924fd41913c7d29e844ba5f48a1494c66d7059 Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Fri, 12 Oct 2018 11:39:21 -0400 Subject: [PATCH 4/8] devops: Perform HEAD call solely on Devops/VSTS accounts --- .../Src/Authentication.cs | 206 +++++++++--------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/AzureDevOps.Authentication/Src/Authentication.cs b/AzureDevOps.Authentication/Src/Authentication.cs index 8b62767ed..e3986c60f 100644 --- a/AzureDevOps.Authentication/Src/Authentication.cs +++ b/AzureDevOps.Authentication/Src/Authentication.cs @@ -177,148 +177,148 @@ public override async Task DeleteCredentials(TargetUri targetUri) if (targetUri is null) throw new ArgumentNullException(nameof(targetUri)); - // Assume Azure DevOps using Azure "common tenant" (empty GUID). - var tenantId = Guid.Empty; - - // Compose the request Uri, by default it is the target Uri. - var requestUri = targetUri; - - // Override the request Uri, when actual Uri exists, with actual Uri. - if (targetUri.ActualUri != null) + if (IsAzureDevOpsUrl(targetUri)) { - requestUri = targetUri.CreateWith(queryUri: targetUri.ActualUri); - } + // Assume Azure DevOps using Azure "common tenant" (empty GUID). + var tenantId = Guid.Empty; - // If the protocol (aka scheme) being used isn't HTTP based, there's no point in - // querying the server, so skip that work. - if (OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttp) - || OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttps)) - { - var requestUrl = GetTargetUrl(requestUri, false); + // Compose the request Uri, by default it is the target Uri. + var requestUri = targetUri; - // Read the cache from disk. - var cache = await DeserializeTenantCache(context); + // Override the request Uri, when actual Uri exists, with actual Uri. + if (targetUri.ActualUri != null) + { + requestUri = targetUri.CreateWith(queryUri: targetUri.ActualUri); + } - // Check the cache for an existing value. - if (cache.TryGetValue(requestUrl, out tenantId)) + // If the protocol (aka scheme) being used isn't HTTP based, there's no point in + // querying the server, so skip that work. + if (OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttp) + || OrdinalIgnoreCase.Equals(requestUri.Scheme, Uri.UriSchemeHttps)) { - context.Trace.WriteLine($"'{requestUrl}' is Azure DevOps, tenant resource is {{{tenantId.ToString("N")}}}."); + var requestUrl = GetTargetUrl(requestUri, false); - return tenantId; - } + // Read the cache from disk. + var cache = await DeserializeTenantCache(context); - // Use the properly formatted URL - requestUri = requestUri.CreateWith(queryUrl: requestUrl); + // Check the cache for an existing value. + if (cache.TryGetValue(requestUrl, out tenantId)) + { + context.Trace.WriteLine($"'{requestUrl}' is Azure DevOps, tenant resource is {{{tenantId.ToString("N")}}}."); - var options = new NetworkRequestOptions(false) - { - Flags = NetworkRequestOptionFlags.UseProxy, - Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), - }; + return tenantId; + } - try - { - // Query the host use the response headers to determine if the host is Azure DevOps or not. - using (var response = await context.Network.HttpHeadAsync(requestUri, options)) + // Use the properly formatted URL + requestUri = requestUri.CreateWith(queryUrl: requestUrl); + + var options = new NetworkRequestOptions(false) { - if (response.Headers != null) + Flags = NetworkRequestOptionFlags.UseProxy, + Timeout = TimeSpan.FromMilliseconds(Global.RequestTimeout), + }; + + try + { + // Query the host use the response headers to determine if the host is Azure DevOps or not. + using (var response = await context.Network.HttpHeadAsync(requestUri, options)) { - // If the "X-VSS-ResourceTenant" was returned, then it is Azure DevOps and we'll need it's value. - if (response.Headers.TryGetValues(XvssResourceTenantHeader, out IEnumerable values)) + if (response.Headers != null) { - context.Trace.WriteLine($"detected '{requestUrl}' as Azure DevOps from GET response."); - - // The "Www-Authenticate" is a more reliable header, because it indicates the - // authentication scheme that should be used to access the requested entity. - if (response.Headers.WwwAuthenticate != null) + // If the "X-VSS-ResourceTenant" was returned, then it is Azure DevOps and we'll need it's value. + if (response.Headers.TryGetValues(XvssResourceTenantHeader, out IEnumerable values)) { - foreach (var header in response.Headers.WwwAuthenticate) - { - const string AuthorizationUriPrefix = "authorization_uri="; - - var value = header.Parameter; + context.Trace.WriteLine($"detected '{requestUrl}' as Azure DevOps from GET response."); - if (value?.Length >= AuthorizationUriPrefix.Length + AuthorityHostUrlBase.Length + GuidStringLength) + // The "Www-Authenticate" is a more reliable header, because it indicates the + // authentication scheme that should be used to access the requested entity. + if (response.Headers.WwwAuthenticate != null) + { + foreach (var header in response.Headers.WwwAuthenticate) { - // The header parameter will look something like "authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47" - // and all we want is the portion after the '=' and before the last '/'. - int index1 = value.IndexOf('=', AuthorizationUriPrefix.Length - 1); - int index2 = value.LastIndexOf('/'); + const string AuthorizationUriPrefix = "authorization_uri="; - // Parse the header value if the necessary characters exist... - if (index1 > 0 && index2 > index1) + var value = header.Parameter; + + if (value?.Length >= AuthorizationUriPrefix.Length + AuthorityHostUrlBase.Length + GuidStringLength) { - var authorityUrl = value.Substring(index1 + 1, index2 - index1 - 1); - var guidString = value.Substring(index2 + 1, GuidStringLength); + // The header parameter will look something like "authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47" + // and all we want is the portion after the '=' and before the last '/'. + int index1 = value.IndexOf('=', AuthorizationUriPrefix.Length - 1); + int index2 = value.LastIndexOf('/'); - // If the authority URL is as expected, attempt to parse the tenant resource identity. - if (OrdinalIgnoreCase.Equals(authorityUrl, AuthorityHostUrlBase) - && Guid.TryParse(guidString, out tenantId)) + // Parse the header value if the necessary characters exist... + if (index1 > 0 && index2 > index1) { - // Update the cache. - cache[requestUrl] = tenantId; - - // Write the cache to disk. - await SerializeTenantCache(context, cache); - - // Since we found a value, break the loop (likely a loop of one item anyways). - break; + var authorityUrl = value.Substring(index1 + 1, index2 - index1 - 1); + var guidString = value.Substring(index2 + 1, GuidStringLength); + + // If the authority URL is as expected, attempt to parse the tenant resource identity. + if (OrdinalIgnoreCase.Equals(authorityUrl, AuthorityHostUrlBase) + && Guid.TryParse(guidString, out tenantId)) + { + // Update the cache. + cache[requestUrl] = tenantId; + + // Write the cache to disk. + await SerializeTenantCache(context, cache); + + // Since we found a value, break the loop (likely a loop of one item anyways). + break; + } } } } } - } - else - { - // Since there wasn't a "Www-Authenticate" header returned - // iterate through the values, taking the first non-zero value. - foreach (string value in values) + else { - // Try to find a value for the resource-tenant identity. - // Given that some projects will return multiple tenant identities, - if (!string.IsNullOrWhiteSpace(value) - && Guid.TryParse(value, out tenantId)) + // Since there wasn't a "Www-Authenticate" header returned + // iterate through the values, taking the first non-zero value. + foreach (string value in values) { - // Update the cache. - cache[requestUrl] = tenantId; + // Try to find a value for the resource-tenant identity. + // Given that some projects will return multiple tenant identities, + if (!string.IsNullOrWhiteSpace(value) + && Guid.TryParse(value, out tenantId)) + { + // Update the cache. + cache[requestUrl] = tenantId; - // Write the cache to disk. - await SerializeTenantCache(context, cache); + // Write the cache to disk. + await SerializeTenantCache(context, cache); - // Break the loop if a non-zero value has been detected. - if (tenantId != Guid.Empty) - { - break; + // Break the loop if a non-zero value has been detected. + if (tenantId != Guid.Empty) + { + break; + } } } } - } - context.Trace.WriteLine($"tenant resource for '{requestUrl}' is {{{tenantId.ToString("N")}}}."); + context.Trace.WriteLine($"tenant resource for '{requestUrl}' is {{{tenantId.ToString("N")}}}."); - // Return the tenant identity to the caller because this is Azure DevOps. - return tenantId; + // Return the tenant identity to the caller because this is Azure DevOps. + return tenantId; + } + } + else + { + context.Trace.WriteLine($"unable to get response from '{requestUri}' [{(int)response.StatusCode} {response.StatusCode}]."); } } - else - { - context.Trace.WriteLine($"unable to get response from '{requestUri}' [{(int)response.StatusCode} {response.StatusCode}]."); - } + } + catch (HttpRequestException exception) + { + context.Trace.WriteLine($"unable to get response from '{requestUri}', an error occurred before the server could respond."); + context.Trace.WriteException(exception); } } - catch (HttpRequestException exception) + else { - context.Trace.WriteLine($"unable to get response from '{requestUri}', an error occurred before the server could respond."); - context.Trace.WriteException(exception); + context.Trace.WriteLine($"detected non-http(s) based protocol: '{requestUri.Scheme}'."); } } - else - { - context.Trace.WriteLine($"detected non-http(s) based protocol: '{requestUri.Scheme}'."); - } - - if (OrdinalIgnoreCase.Equals(VstsBaseUrlHost, requestUri.Host)) - return Guid.Empty; // Fallback to basic authentication. return null; From b316ea51bed846b74d608a71bd97598fd9b227a0 Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Fri, 12 Oct 2018 13:10:05 -0400 Subject: [PATCH 5/8] devops: Update logging to better reflect urls used --- AzureDevOps.Authentication/Src/Authority.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/AzureDevOps.Authentication/Src/Authority.cs b/AzureDevOps.Authentication/Src/Authority.cs index 4aaf8958f..6d9fbb8e9 100644 --- a/AzureDevOps.Authentication/Src/Authority.cs +++ b/AzureDevOps.Authentication/Src/Authority.cs @@ -289,7 +289,7 @@ public async Task PopulateTokenTargetId(TargetUri targetUri, Token authori } } - Trace.WriteLine($"failed to acquire the token's target identity for `{targetUri}` [{(int)response.StatusCode}]."); + Trace.WriteLine($"failed to acquire the token's target identity for `{requestUri?.QueryUri}` [{(int)response.StatusCode}]."); } } catch (HttpRequestException exception) @@ -372,13 +372,13 @@ internal async Task GetIdentityServiceUri(TargetUri targetUri, Secret } } - Trace.WriteLine($"failed to find Identity Service for '{targetUri}' via location service [{(int)response.StatusCode}]."); + Trace.WriteLine($"failed to find Identity Service for '{requestUri?.QueryUri}' via location service [{(int)response.StatusCode}]."); } } catch (Exception exception) { Trace.WriteException(exception); - throw new LocationServiceException($"Helper for `{targetUri}`.", exception); + throw new LocationServiceException($"Helper for `{requestUri?.QueryUri}`.", exception); } return null; @@ -454,7 +454,7 @@ internal async Task ValidateSecret(TargetUri targetUri, Secret secret) // If the server responds with content, and said content matches the anonymous details the credentials are invalid. if (content != null && Regex.IsMatch(content, AnonymousUserPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) { - Trace.WriteLine($"credential validation for '{targetUri}' failed."); + Trace.WriteLine($"credential validation for '{requestUri?.QueryUri}' failed."); return false; } @@ -463,7 +463,7 @@ internal async Task ValidateSecret(TargetUri targetUri, Secret secret) if (statusCode >= HttpStatusCode.OK && statusCode < HttpStatusCode.Ambiguous) return true; - Trace.WriteLine($"credential validation for '{targetUri}' failed [{(int)response.StatusCode}]."); + Trace.WriteLine($"credential validation for '{requestUri?.QueryUri}' failed [{(int)response.StatusCode}]."); // Even if the service responded, if the issue isn't a 400 class response then the credentials were likely not rejected. if (statusCode < HttpStatusCode.BadRequest || statusCode >= HttpStatusCode.InternalServerError) @@ -474,20 +474,20 @@ internal async Task ValidateSecret(TargetUri targetUri, Secret secret) { // Since we're unable to invalidate the credentials, return optimistic results. // This avoid credential invalidation due to network instability, etc. - Trace.WriteLine($"unable to validate credentials for '{targetUri}', failure occurred before server could respond."); + Trace.WriteLine($"unable to validate credentials for '{requestUri?.QueryUri}', failure occurred before server could respond."); Trace.WriteException(exception); return true; } catch (Exception exception) { - Trace.WriteLine($"credential validation for '{targetUri}' failed."); + Trace.WriteLine($"credential validation for '{requestUri?.QueryUri}' failed."); Trace.WriteException(exception); return false; } - Trace.WriteLine($"credential validation for '{targetUri}' failed."); + Trace.WriteLine($"credential validation for '{requestUri?.QueryUri}' failed."); return false; } @@ -530,7 +530,7 @@ private StringContent GetAccessTokenRequestBody(TargetUri targetUri, TokenScope string tokenUrl = GetTargetUrl(targetUri, false); - Trace.WriteLine($"creating access token scoped to '{tokenScope}' for '{targetUri}'"); + Trace.WriteLine($"creating access token scoped to '{tokenScope}' for '{tokenUrl}'"); string jsonContent = (duration.HasValue && duration.Value > TimeSpan.FromHours(1)) ? string.Format(Culture.InvariantCulture, ContentTimedJsonFormat, tokenScope, tokenUrl, Settings.MachineName, DateTime.UtcNow + duration.Value) From 8bbb682e7bb16f096298f183f57d7bb94319f98f Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Fri, 12 Oct 2018 13:30:29 -0400 Subject: [PATCH 6/8] update version numbers (v1.18.2) --- Cli/Askpass/Properties/AssemblyInfo.cs | 4 ++-- Cli/Manager/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cli/Askpass/Properties/AssemblyInfo.cs b/Cli/Askpass/Properties/AssemblyInfo.cs index e69e61aae..1f4b017b2 100644 --- a/Cli/Askpass/Properties/AssemblyInfo.cs +++ b/Cli/Askpass/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-1230-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.1.0")] -[assembly: AssemblyFileVersion("1.18.1.0")] +[assembly: AssemblyVersion("1.18.2.0")] +[assembly: AssemblyFileVersion("1.18.2.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed. diff --git a/Cli/Manager/Properties/AssemblyInfo.cs b/Cli/Manager/Properties/AssemblyInfo.cs index fc9944bfc..0289d4506 100644 --- a/Cli/Manager/Properties/AssemblyInfo.cs +++ b/Cli/Manager/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-0415-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.1.0")] -[assembly: AssemblyFileVersion("1.18.1.0")] +[assembly: AssemblyVersion("1.18.2.0")] +[assembly: AssemblyFileVersion("1.18.2.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed. From 31b8182201c430c2a1bbb8b5eb4183096ce69fd2 Mon Sep 17 00:00:00 2001 From: Mike Minns Date: Wed, 24 Oct 2018 13:30:46 +0100 Subject: [PATCH 7/8] Issue-799 Refactor Settings.GetEnvironmentVariables() to allow for the de-duplication of Environment Variables, e.g. differeing cased keys, with associated test --- Microsoft.Alm.Authentication/Src/Settings.cs | 11 ++++- .../Microsoft.Alm.Authentication.Test.csproj | 1 + .../Test/SettingsTest.cs | 48 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 Microsoft.Alm.Authentication/Test/SettingsTest.cs diff --git a/Microsoft.Alm.Authentication/Src/Settings.cs b/Microsoft.Alm.Authentication/Src/Settings.cs index 821c292e4..18506d9a1 100644 --- a/Microsoft.Alm.Authentication/Src/Settings.cs +++ b/Microsoft.Alm.Authentication/Src/Settings.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using static System.StringComparer; @@ -79,13 +80,19 @@ public string[] GetCommandLineArgs() public IDictionary GetEnvironmentVariables(EnvironmentVariableTarget target) { var variables = Environment.GetEnvironmentVariables(target); + return DeduplicateStringDictionary(variables); + } + + internal IDictionary DeduplicateStringDictionary(IDictionary variables) + { var result = new Dictionary(variables.Count, OrdinalIgnoreCase); - foreach(var key in variables.Keys) + foreach (var key in variables.Keys) { if (key is string name && variables[key] is string value) { - result.Add(name, value); + // avoid trying to add duplicates, e.g. different case names, last entry wins + result[name] = value; } } diff --git a/Microsoft.Alm.Authentication/Test/Microsoft.Alm.Authentication.Test.csproj b/Microsoft.Alm.Authentication/Test/Microsoft.Alm.Authentication.Test.csproj index 984e3ad01..a0860e485 100644 --- a/Microsoft.Alm.Authentication/Test/Microsoft.Alm.Authentication.Test.csproj +++ b/Microsoft.Alm.Authentication/Test/Microsoft.Alm.Authentication.Test.csproj @@ -54,6 +54,7 @@ + diff --git a/Microsoft.Alm.Authentication/Test/SettingsTest.cs b/Microsoft.Alm.Authentication/Test/SettingsTest.cs new file mode 100644 index 000000000..9adcb9f51 --- /dev/null +++ b/Microsoft.Alm.Authentication/Test/SettingsTest.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Alm.Authentication.Test +{ + public class SettingsTest + { + private readonly ITestOutputHelper _output; + + public SettingsTest(ITestOutputHelper testOutputHelper) + { + _output = testOutputHelper; + } + + [Fact] + public void VerifyDeduplicateStringDictionaryStripsEntriesWithDuplicatedByCaseKeys() + { + var vars = new Dictionary() + { + { "home", "value-1" }, + { "Home", "value-2" }, + { "HOme", "value-3" }, + { "HOMe", "value-4" }, + { "HOME", "value-5" }, + { "homE", "value-6" }, + { "away", "value-7" }, + }; + + var settings = new Settings(RuntimeContext.Default); + var result = settings.DeduplicateStringDictionary(vars); + + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Equal("home", result.First().Key); + Assert.Equal("value-6", result.First().Value); + Assert.Equal("value-6", result["HOME"]); + Assert.Equal("away", result.Last().Key); + Assert.Equal("value-7", result.Last().Value); + Assert.Equal("value-7", result["AwAy"]); + + } + } +} \ No newline at end of file From b736ec3bcc7add2b76836d948371c6d5257d59dc Mon Sep 17 00:00:00 2001 From: Jessica Schumaker Date: Fri, 2 Nov 2018 13:14:04 -0400 Subject: [PATCH 8/8] version: update assembly version 1.18.4 --- Cli/Askpass/Properties/AssemblyInfo.cs | 4 ++-- Cli/Manager/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cli/Askpass/Properties/AssemblyInfo.cs b/Cli/Askpass/Properties/AssemblyInfo.cs index 61a36c4b8..5ae89b820 100644 --- a/Cli/Askpass/Properties/AssemblyInfo.cs +++ b/Cli/Askpass/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-1230-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.0.0")] -[assembly: AssemblyFileVersion("1.18.0.0")] +[assembly: AssemblyVersion("1.18.4.0")] +[assembly: AssemblyFileVersion("1.18.4.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed. diff --git a/Cli/Manager/Properties/AssemblyInfo.cs b/Cli/Manager/Properties/AssemblyInfo.cs index c69d64458..cbaae105d 100644 --- a/Cli/Manager/Properties/AssemblyInfo.cs +++ b/Cli/Manager/Properties/AssemblyInfo.cs @@ -12,8 +12,8 @@ [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] [assembly: Guid("19770407-63d4-0415-a9df-f1c4b473308a")] -[assembly: AssemblyVersion("1.18.0.0")] -[assembly: AssemblyFileVersion("1.18.0.0")] +[assembly: AssemblyVersion("1.18.4.0")] +[assembly: AssemblyFileVersion("1.18.4.0")] [assembly: NeutralResourcesLanguage("en-US")] // Only expose internals when the binary isn't signed.