diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs index 1b6abc9f27ab..9e076cefc3fa 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs @@ -13,7 +13,7 @@ public class GetCurrentOpenApiReferenceTest public void Execute_ReturnsExpectedItem() { // Arrange - string input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" + + var input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" + "CodeGenerator=NSwagCSharp|Namespace=ConsoleClient|Options=|OutputPath=" + "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs|" + "OriginalItemSpec=../files/azureMonitor.json|FirstForGenerator=true"; @@ -22,8 +22,8 @@ public void Execute_ReturnsExpectedItem() Input = input, }; - string expectedIdentity = "../files/azureMonitor.json"; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedIdentity = "../files/azureMonitor.json"; + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "azureMonitorClient" }, { "CodeGenerator", "NSwagCSharp" }, diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs index 7bc5cc7fb97e..3602ea1c56cb 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs @@ -27,7 +27,7 @@ public void Execute_AddsExpectedMetadata() OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "NSwagClient" }, { "CodeGenerator", "NSwagCSharp" }, @@ -85,7 +85,7 @@ public void Execute_DoesNotOverrideClassName() OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -143,7 +143,7 @@ public void Execute_DoesNotOverrideNamespace() OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "NSwagClient" }, { "CodeGenerator", "NSwagCSharp" }, @@ -201,7 +201,7 @@ public void Execute_DoesNotOverrideOutputPath_IfRooted() OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -351,7 +351,7 @@ public void Execute_SetsClassName_BasedOnOutputPath() OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -414,7 +414,7 @@ public void Execute_SetsClassName_BasedOnSanitizedOutputPath(string outputPath, OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -481,7 +481,7 @@ public void Execute_SetsFirstForGenerator_UsesCorrectExtension() OutputDirectory = "obj", }; - IDictionary expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className12 }, { "CodeGenerator", codeGenerator13 }, @@ -496,7 +496,7 @@ public void Execute_SetsFirstForGenerator_UsesCorrectExtension() $"OutputPath={outputPath1}|ClassName={className12}|Namespace={@namespace}" }, }; - IDictionary expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className12 }, { "CodeGenerator", codeGenerator2 }, @@ -511,7 +511,7 @@ public void Execute_SetsFirstForGenerator_UsesCorrectExtension() $"OutputPath={outputPath2}|ClassName={className12}|Namespace={@namespace}" }, }; - IDictionary expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className3 }, { "CodeGenerator", codeGenerator13 }, diff --git a/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs new file mode 100644 index 000000000000..adc889ccacc9 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs @@ -0,0 +1,314 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.ApiDescription.Client +{ + // ItemSpec values always have '\\' converted to '/' on input when running on non-Windows. It is not possible to + // retrieve the original (unconverted) item spec value. In other respects, item spec values are treated identically + // to custom metadata values. + // + // ITaskItem members aka the implicitly-implemented methods and properties in TaskItem expect _escaped_ values on + // input and return _literal_ values. This includes TaskItem constructors and CloneCustomMetadata() (which returns + // a new dictionary containing literal values). TaskItem stores all values in their escaped form. + // + // Added ITaskItem2 members e.g. CloneCustomMetadataEscaped(), GetMetadataValueEscaped(...) and + // EvaluatedIncludeEscaped return escaped values. Of all TaskItem methods, only SetMetadataValueLiteral(...) + // accepts a literal input value. + // + // Metadata names are never escaped. + // + // MetadataSerializer expects literal values on input. + public class MetadataSerializerTest + { + // Maps literal to escaped values. + public static TheoryData EscapedValuesMapping { get; } = new TheoryData + { + { "No escaping necessary for =.", "No escaping necessary for =." }, + { "Value needs escaping? (yes)", "Value needs escaping%3f %28yes%29" }, + { "$ comes earlier; @ comes later.", "%24 comes earlier%3b %40 comes later." }, + { + "A '%' *character* needs escaping %-escaping.", + "A %27%25%27 %2acharacter%2a needs escaping %25-escaping." + }, + }; + + public static TheoryData EscapedValues + { + get + { + var result = new TheoryData(); + foreach (var entry in EscapedValuesMapping) + { + result.Add((string)entry[1]); + } + + return result; + } + } + + public static TheoryData LiteralValues + { + get + { + var result = new TheoryData(); + foreach (var entry in EscapedValuesMapping) + { + result.Add((string)entry[0]); + } + + return result; + } + } + + [Theory] + [MemberData(nameof(LiteralValues))] + public void SetMetadata_UpdatesTaskAsExpected(string value) + { + // Arrange + var item = new TaskItem("My Identity"); + var key = "My key"; + + // Act + MetadataSerializer.SetMetadata(item, key, value); + + // Assert + Assert.Equal(value, item.GetMetadata(key)); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SetMetadata_UpdatesTaskAsExpected_WithLegacyItem(string value, string escapedValue) + { + // Arrange + var item = new Mock(MockBehavior.Strict); + var key = "My key"; + item.Setup(i => i.SetMetadata(key, escapedValue)).Verifiable(); + + // Act + MetadataSerializer.SetMetadata(item.Object, key, value); + + // Assert + item.Verify(i => i.SetMetadata(key, escapedValue), Times.Once); + } + + [Fact] + public void DeserializeMetadata_ReturnsExpectedTask() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var input = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(identity, item.ItemSpec); + var metadata = Assert.IsAssignableFrom>(item.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void DeserializeMetadata_ReturnsExpectedTask_WhenEscaping(string value, string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var input = $"Identity={identity}|Value={escapedValue}"; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(identity, item.ItemSpec); + Assert.Equal(value, item.GetMetadata("Value")); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void DeserializeMetadata_ReturnsExpectedTask_WhenEscapingIdentity(string value, string escapedValue) + { + // Arrange + var input = $"Identity={escapedValue}|Value=a value"; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(value, item.ItemSpec); + Assert.Equal("a value", item.GetMetadata("Value")); + } + + [Fact] + public void SerializeMetadata_ReturnsExpectedString() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + var input = new TaskItem(identity, metadata); + var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData(nameof(EscapedValues))] + public void SerializeMetadata_ReturnsExpectedString_WhenEscaping(string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var expectedResult = $"Identity={identity}|Value={escapedValue}"; + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", escapedValue } }; + var input = new TaskItem(identity, metadata); + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData(nameof(EscapedValues))] + public void SerializeMetadata_ReturnsExpectedString_WhenEscapingIdentity(string escapedValue) + { + // Arrange + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } }; + var expectedResult = $"Identity={escapedValue}|Value=a value"; + var input = new TaskItem(escapedValue, metadata); + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscaping( + string value, + string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", value } }; + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={identity}|Value={escapedValue}"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscapingIdentity( + string value, + string escapedValue) + { + // Arrange + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } }; + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(value).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={escapedValue}|Value=a value"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + } +}