// 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 System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Formatters.Xml { public class XmlSerializerOutputFormatterTest { public class DummyClass { public int SampleInt { get; set; } } public class TestLevelOne { public int SampleInt { get; set; } public string sampleString; } public class TestLevelTwo { public string SampleString { get; set; } public TestLevelOne TestOne { get; set; } } public static IEnumerable BasicTypeValues { get { yield return new object[] { "sampleString", "sampleString" }; yield return new object[] { 5, "5" }; yield return new object[] { 5.43, "5.43" }; yield return new object[] { 'a', "97" }; yield return new object[] { new DummyClass { SampleInt = 10 }, "10" }; } } [Theory] [MemberData(nameof(BasicTypeValues))] public async Task XmlSerializerOutputFormatterCanWriteBasicTypes(object input, string expectedOutput) { // Arrange var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(input, input.GetType()); // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public void XmlSerializer_CachesSerializerForType() { // Arrange var input = new DummyClass { SampleInt = 10 }; var formatter = new TestXmlSerializerOutputFormatter(); var context = GetOutputFormatterContext(input, typeof(DummyClass)); context.ContentType = new StringSegment("application/xml"); // Act formatter.CanWriteResult(context); formatter.CanWriteResult(context); // Assert Assert.Equal(1, formatter.createSerializerCalledCount); } [Fact] public void DefaultConstructor_ExpectedWriterSettings_Created() { // Arrange and Act var formatter = new XmlSerializerOutputFormatter(); // Assert var writerSettings = formatter.WriterSettings; Assert.NotNull(writerSettings); Assert.True(writerSettings.OmitXmlDeclaration); Assert.False(writerSettings.CloseOutput); Assert.False(writerSettings.CheckCharacters); } [Fact] public async Task SuppliedWriterSettings_TakeAffect() { // Arrange var writerSettings = FormattingUtilities.GetDefaultXmlWriterSettings(); writerSettings.OmitXmlDeclaration = false; var sampleInput = new DummyClass { SampleInt = 10 }; var formatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); var formatter = new XmlSerializerOutputFormatter(writerSettings); var expectedOutput = "" + "10"; // Act await formatter.WriteAsync(formatterContext); // Assert var body = formatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task XmlSerializerOutputFormatterWritesSimpleTypes() { // Arrange var expectedOutput = "10"; var sampleInput = new DummyClass { SampleInt = 10 }; var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task XmlSerializerOutputFormatterWritesComplexTypes() { // Arrange var expectedOutput = "TestString" + "TestLevelOne string" + "10"; var sampleInput = new TestLevelTwo { SampleString = "TestString", TestOne = new TestLevelOne { SampleInt = 10, sampleString = "TestLevelOne string" } }; var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task XmlSerializerOutputFormatterWritesOnModifiedWriterSettings() { // Arrange var expectedOutput = "" + "10"; var sampleInput = new DummyClass { SampleInt = 10 }; var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); var formatter = new XmlSerializerOutputFormatter( new System.Xml.XmlWriterSettings { OmitXmlDeclaration = false, CloseOutput = false }); // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task XmlSerializerOutputFormatterWritesUTF16Output() { // Arrange var expectedOutput = "" + "10"; var sampleInput = new DummyClass { SampleInt = 10 }; var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType(), "application/xml; charset=utf-16"); var formatter = new XmlSerializerOutputFormatter(); formatter.WriterSettings.OmitXmlDeclaration = false; // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader( body, new UnicodeEncoding(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true)).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task XmlSerializerOutputFormatterWritesIndentedOutput() { // Arrange var expectedOutput = "\r\n 10\r\n"; var sampleInput = new DummyClass { SampleInt = 10 }; var formatter = new XmlSerializerOutputFormatter(); formatter.WriterSettings.Indent = true; var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); // Act await formatter.WriteAsync(outputFormatterContext); // Assert var body = outputFormatterContext.HttpContext.Response.Body; body.Position = 0; var content = new StreamReader(body).ReadToEnd(); XmlAssert.Equal(expectedOutput, content); } [Fact] public async Task VerifyBodyIsNotClosedAfterOutputIsWritten() { // Arrange var sampleInput = new DummyClass { SampleInt = 10 }; var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); // Act await formatter.WriteAsync(outputFormatterContext); // Assert Assert.NotNull(outputFormatterContext.HttpContext.Response.Body); Assert.True(outputFormatterContext.HttpContext.Response.Body.CanRead); } public static IEnumerable TypesForCanWriteResult { get { yield return new object[] { null, typeof(string), true }; yield return new object[] { null, null, false }; yield return new object[] { new DummyClass { SampleInt = 5 }, typeof(DummyClass), true }; yield return new object[] { null, typeof(object), true }; yield return new object[] { new Dictionary { { "Hello", "world" } }, typeof(Dictionary), false }; yield return new object[] { new[] {"value1", "value2"}, typeof(IEnumerable), true }; yield return new object[] { Enumerable.Range(1, 2).Select(i => "value" + i).AsQueryable(), typeof(IQueryable), true }; } } [Theory] [MemberData(nameof(TypesForCanWriteResult))] public void XmlSerializer_CanWriteResult(object input, Type declaredType, bool expectedOutput) { // Arrange var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(input, declaredType); outputFormatterContext.ContentType = new StringSegment("application/xml"); // Act var result = formatter.CanWriteResult(outputFormatterContext); // Assert Assert.Equal(expectedOutput, result); } [Fact] public async Task XmlSerializerOutputFormatterDoesntFlushOutputStream() { // Arrange var sampleInput = new DummyClass { SampleInt = 10 }; var formatter = new XmlSerializerOutputFormatter(); var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); var response = outputFormatterContext.HttpContext.Response; response.Body = FlushReportingStream.GetThrowingStream(); // Act & Assert await formatter.WriteAsync(outputFormatterContext); } public static IEnumerable TypesForGetSupportedContentTypes { get { yield return new object[] { typeof(DummyClass), "application/xml" }; yield return new object[] { typeof(object), "application/xml" }; yield return new object[] { null, null }; } } [Theory] [MemberData(nameof(TypesForGetSupportedContentTypes))] public void XmlSerializer_GetSupportedContentTypes_Returns_SupportedTypes(Type type, object expectedOutput) { // Arrange var formatter = new XmlSerializerOutputFormatter(); // Act var result = formatter.GetSupportedContentTypes("application/xml", type); // Assert if (expectedOutput != null) { Assert.Equal(expectedOutput, Assert.Single(result).ToString()); } else { Assert.Equal(expectedOutput, result); } } private OutputFormatterWriteContext GetOutputFormatterContext( object outputValue, Type outputType, string contentType = "application/xml; charset=utf-8") { return new OutputFormatterWriteContext( GetHttpContext(contentType), new TestHttpResponseStreamWriterFactory().CreateWriter, outputType, outputValue); } private static HttpContext GetHttpContext(string contentType) { var request = new Mock(); var headers = new HeaderDictionary(); headers["Accept-Charset"] = MediaTypeHeaderValue.Parse(contentType).Charset; request.Setup(r => r.ContentType).Returns(contentType); request.SetupGet(r => r.Headers).Returns(headers); var response = new Mock(); response.SetupGet(f => f.Body).Returns(new MemoryStream()); var httpContext = new Mock(); httpContext.SetupGet(c => c.Request).Returns(request.Object); httpContext.SetupGet(c => c.Response).Returns(response.Object); return httpContext.Object; } private class TestXmlSerializerOutputFormatter : XmlSerializerOutputFormatter { public int createSerializerCalledCount = 0; protected override XmlSerializer CreateSerializer(Type type) { createSerializerCalledCount++; return base.CreateSerializer(type); } } } }