//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Test.Messaging { using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Test.Mocks; using Moq; using NUnit.Framework; [TestFixture] public class MessagingUtilitiesTests : TestBase { [Test] public void CreateQueryString() { var args = new Dictionary(); args.Add("a", "b"); args.Add("c/d", "e/f"); Assert.AreEqual("a=b&c%2Fd=e%2Ff", MessagingUtilities.CreateQueryString(args)); } [Test] public void CreateQueryStringEmptyCollection() { Assert.AreEqual(0, MessagingUtilities.CreateQueryString(new Dictionary()).Length); } [Test, ExpectedException(typeof(ArgumentNullException))] public void CreateQueryStringNullDictionary() { MessagingUtilities.CreateQueryString(null); } [Test] public void AppendQueryArgs() { UriBuilder uri = new UriBuilder("http://baseline.org/page"); var args = new Dictionary(); args.Add("a", "b"); args.Add("c/d", "e/f"); MessagingUtilities.AppendQueryArgs(uri, args); Assert.AreEqual("http://baseline.org/page?a=b&c%2Fd=e%2Ff", uri.Uri.AbsoluteUri); args.Clear(); args.Add("g", "h"); MessagingUtilities.AppendQueryArgs(uri, args); Assert.AreEqual("http://baseline.org/page?a=b&c%2Fd=e%2Ff&g=h", uri.Uri.AbsoluteUri); } [Test, ExpectedException(typeof(ArgumentNullException))] public void AppendQueryArgsNullUriBuilder() { MessagingUtilities.AppendQueryArgs(null, new Dictionary()); } [Test] public void AppendQueryArgsNullDictionary() { MessagingUtilities.AppendQueryArgs(new UriBuilder(), null); } [Test] public void AsHttpResponseMessage() { var responseContent = new byte[10]; (new Random()).NextBytes(responseContent); var responseStream = new MemoryStream(responseContent); var outgoingResponse = new OutgoingWebResponse(); outgoingResponse.Headers.Add("X-SOME-HEADER", "value"); outgoingResponse.Headers.Add("Content-Length", responseContent.Length.ToString(CultureInfo.InvariantCulture)); outgoingResponse.ResponseStream = responseStream; var httpResponseMessage = outgoingResponse.AsHttpResponseMessage(); Assert.That(httpResponseMessage, Is.Not.Null); Assert.That(httpResponseMessage.Headers.GetValues("X-SOME-HEADER").ToList(), Is.EqualTo(new[] { "value" })); Assert.That( httpResponseMessage.Content.Headers.GetValues("Content-Length").ToList(), Is.EqualTo(new[] { responseContent.Length.ToString(CultureInfo.InvariantCulture) })); var actualContent = new byte[responseContent.Length + 1]; // give the opportunity to provide a bit more data than we expect. var bytesRead = httpResponseMessage.Content.ReadAsStreamAsync().Result.Read(actualContent, 0, actualContent.Length); Assert.That(bytesRead, Is.EqualTo(responseContent.Length)); // verify that only the data we expected came back. var trimmedActualContent = new byte[bytesRead]; Array.Copy(actualContent, trimmedActualContent, bytesRead); Assert.That(trimmedActualContent, Is.EqualTo(responseContent)); } [Test] public void AsHttpResponseMessageNoContent() { var outgoingResponse = new OutgoingWebResponse(); outgoingResponse.Headers.Add("X-SOME-HEADER", "value"); var httpResponseMessage = outgoingResponse.AsHttpResponseMessage(); Assert.That(httpResponseMessage, Is.Not.Null); Assert.That(httpResponseMessage.Headers.GetValues("X-SOME-HEADER").ToList(), Is.EqualTo(new[] { "value" })); Assert.That(httpResponseMessage.Content, Is.Null); } [Test] public void ToDictionary() { NameValueCollection nvc = new NameValueCollection(); nvc["a"] = "b"; nvc["c"] = "d"; nvc[string.Empty] = "emptykey"; Dictionary actual = MessagingUtilities.ToDictionary(nvc); Assert.AreEqual(nvc.Count, actual.Count); Assert.AreEqual(nvc["a"], actual["a"]); Assert.AreEqual(nvc["c"], actual["c"]); } [Test, ExpectedException(typeof(ArgumentException))] public void ToDictionaryWithNullKey() { NameValueCollection nvc = new NameValueCollection(); nvc[null] = "a"; nvc["b"] = "c"; nvc.ToDictionary(true); } [Test] public void ToDictionaryWithSkippedNullKey() { NameValueCollection nvc = new NameValueCollection(); nvc[null] = "a"; nvc["b"] = "c"; var dictionary = nvc.ToDictionary(false); Assert.AreEqual(1, dictionary.Count); Assert.AreEqual(nvc["b"], dictionary["b"]); } [Test] public void ToDictionaryNull() { Assert.IsNull(MessagingUtilities.ToDictionary(null)); } [Test, ExpectedException(typeof(ArgumentNullException))] public void ApplyHeadersToResponseNullAspNetResponse() { MessagingUtilities.ApplyHeadersToResponse(new WebHeaderCollection(), (HttpResponseBase)null); } [Test, ExpectedException(typeof(ArgumentNullException))] public void ApplyHeadersToResponseNullListenerResponse() { MessagingUtilities.ApplyHeadersToResponse(new WebHeaderCollection(), (HttpListenerResponse)null); } [Test, ExpectedException(typeof(ArgumentNullException))] public void ApplyHeadersToResponseNullHeaders() { MessagingUtilities.ApplyHeadersToResponse(null, new HttpResponseWrapper(new HttpResponse(new StringWriter()))); } [Test] public void ApplyHeadersToResponse() { var headers = new WebHeaderCollection(); headers[HttpResponseHeader.ContentType] = "application/binary"; var response = new HttpResponseWrapper(new HttpResponse(new StringWriter())); MessagingUtilities.ApplyHeadersToResponse(headers, response); Assert.AreEqual(headers[HttpResponseHeader.ContentType], response.ContentType); } /// /// Verifies RFC 3986 compliant URI escaping, as required by the OpenID and OAuth specifications. /// /// /// The tests in this method come from http://wiki.oauth.net/TestCases /// [Test] public void EscapeUriDataStringRfc3986Tests() { Assert.AreEqual("abcABC123", MessagingUtilities.EscapeUriDataStringRfc3986("abcABC123")); Assert.AreEqual("-._~", MessagingUtilities.EscapeUriDataStringRfc3986("-._~")); Assert.AreEqual("%25", MessagingUtilities.EscapeUriDataStringRfc3986("%")); Assert.AreEqual("%2B", MessagingUtilities.EscapeUriDataStringRfc3986("+")); Assert.AreEqual("%26%3D%2A", MessagingUtilities.EscapeUriDataStringRfc3986("&=*")); Assert.AreEqual("%0A", MessagingUtilities.EscapeUriDataStringRfc3986("\n")); Assert.AreEqual("%20", MessagingUtilities.EscapeUriDataStringRfc3986(" ")); Assert.AreEqual("%7F", MessagingUtilities.EscapeUriDataStringRfc3986("\u007f")); Assert.AreEqual("%C2%80", MessagingUtilities.EscapeUriDataStringRfc3986("\u0080")); Assert.AreEqual("%E3%80%81", MessagingUtilities.EscapeUriDataStringRfc3986("\u3001")); } /// /// Verifies the overall format of the multipart POST is correct. /// [Test] public void PostMultipart() { var httpHandler = new TestWebRequestHandler(); bool callbackTriggered = false; httpHandler.Callback = req => { var m = Regex.Match(req.ContentType, "multipart/form-data; boundary=(.+)"); Assert.IsTrue(m.Success, "Content-Type HTTP header not set correctly."); string boundary = m.Groups[1].Value; boundary = boundary.Substring(0, boundary.IndexOf(';')); // trim off charset string expectedEntity = "--{0}\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nb\r\n--{0}--\r\n"; expectedEntity = string.Format(expectedEntity, boundary); string actualEntity = httpHandler.RequestEntityAsString; Assert.AreEqual(expectedEntity, actualEntity); callbackTriggered = true; Assert.AreEqual(req.ContentLength, actualEntity.Length); IncomingWebResponse resp = new CachedDirectWebResponse(); return resp; }; var request = (HttpWebRequest)WebRequest.Create("http://someserver"); var parts = new[] { MultipartPostPart.CreateFormPart("a", "b"), }; request.PostMultipart(httpHandler, parts); Assert.IsTrue(callbackTriggered); } /// /// Verifies proper behavior of GetHttpVerb /// [Test] public void GetHttpVerbTest() { Assert.AreEqual("GET", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.GetRequest)); Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest)); Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest)); Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest)); Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest)); Assert.AreEqual("PATCH", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PatchRequest)); Assert.AreEqual("OPTIONS", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.OptionsRequest)); Assert.AreEqual("GET", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("PATCH", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PatchRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("OPTIONS", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.OptionsRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); } /// /// Verifies proper behavior of GetHttpVerb on invalid input. /// [Test, ExpectedException(typeof(ArgumentException))] public void GetHttpVerbOutOfRangeTest() { MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.PostRequest); } /// /// Verifies proper behavior of GetHttpDeliveryMethod /// [Test] public void GetHttpDeliveryMethodTest() { Assert.AreEqual(HttpDeliveryMethods.GetRequest, MessagingUtilities.GetHttpDeliveryMethod("GET")); Assert.AreEqual(HttpDeliveryMethods.PostRequest, MessagingUtilities.GetHttpDeliveryMethod("POST")); Assert.AreEqual(HttpDeliveryMethods.HeadRequest, MessagingUtilities.GetHttpDeliveryMethod("HEAD")); Assert.AreEqual(HttpDeliveryMethods.PutRequest, MessagingUtilities.GetHttpDeliveryMethod("PUT")); Assert.AreEqual(HttpDeliveryMethods.DeleteRequest, MessagingUtilities.GetHttpDeliveryMethod("DELETE")); Assert.AreEqual(HttpDeliveryMethods.PatchRequest, MessagingUtilities.GetHttpDeliveryMethod("PATCH")); Assert.AreEqual(HttpDeliveryMethods.OptionsRequest, MessagingUtilities.GetHttpDeliveryMethod("OPTIONS")); } /// /// Verifies proper behavior of GetHttpDeliveryMethod for an unexpected input /// [Test, ExpectedException(typeof(ArgumentException))] public void GetHttpDeliveryMethodOutOfRangeTest() { MessagingUtilities.GetHttpDeliveryMethod("UNRECOGNIZED"); } [Test] public void EncryptDecrypt() { const string PlainText = "Hi folks!"; byte[] key = MessagingUtilities.GetCryptoRandomData(128 / 8); var cipher = MessagingUtilities.Encrypt(PlainText, key); Console.WriteLine("Encrypted \"{0}\" ({1} length) to {2} encrypted bytes.", PlainText, PlainText.Length, cipher.Length); string roundTripped = MessagingUtilities.Decrypt(cipher, key); Assert.AreEqual(PlainText, roundTripped); } [Test] public void SerializeAsJsonTest() { var message = new TestMessageWithDate() { Age = 18, Timestamp = DateTime.Parse("4/28/2012"), Name = "Andrew", }; string json = MessagingUtilities.SerializeAsJson(message, this.MessageDescriptions); Assert.That(json, Is.EqualTo("{\"ts\":\"2012-04-28T00:00:00Z\",\"age\":18,\"Name\":\"Andrew\"}")); } [Test] public void DeserializeFromJson() { var message = new TestMessageWithDate(); string json = "{\"ts\":\"2012-04-28T00:00:00Z\",\"age\":18,\"Name\":\"Andrew\"}"; MessagingUtilities.DeserializeFromJson(Encoding.UTF8.GetBytes(json), message, this.MessageDescriptions); Assert.That(message.Age, Is.EqualTo(18)); Assert.That(message.Timestamp, Is.EqualTo(DateTime.Parse("4/28/2012"))); Assert.That(message.Name, Is.EqualTo("Andrew")); } /// /// Verifies that the time-independent string equality check works accurately. /// [Test] public void EqualsConstantTime() { this.EqualsConstantTimeHelper(null, null); this.EqualsConstantTimeHelper(null, string.Empty); this.EqualsConstantTimeHelper(string.Empty, string.Empty); this.EqualsConstantTimeHelper(string.Empty, "a"); this.EqualsConstantTimeHelper(null, "a"); this.EqualsConstantTimeHelper("a", "a"); this.EqualsConstantTimeHelper("a", "A"); this.EqualsConstantTimeHelper("A", "A"); this.EqualsConstantTimeHelper("ab", "ab"); this.EqualsConstantTimeHelper("ab", "b"); } /// /// Verifies that EqualsConstantTime actually has the same execution time regardless of how well a value matches. /// [Test, Category("Performance")] public void EqualsConstantTimeIsActuallyConstantTime() { string expected = new string('A', 5000); string totalmismatch = new string('B', 5000); string almostmatch = new string('A', 4999) + 'B'; const int Iterations = 4000; var totalMismatchTimer = new Stopwatch(); totalMismatchTimer.Start(); for (int i = 0; i < Iterations; i++) { MessagingUtilities.EqualsConstantTime(expected, totalmismatch); } totalMismatchTimer.Stop(); var almostMatchTimer = new Stopwatch(); almostMatchTimer.Start(); for (int i = 0; i < Iterations; i++) { MessagingUtilities.EqualsConstantTime(expected, almostmatch); } almostMatchTimer.Stop(); const double ToleranceFactor = 0.12; long averageTimeTicks = (totalMismatchTimer.ElapsedTicks + almostMatchTimer.ElapsedTicks) / 2; var tolerableDifference = TimeSpan.FromTicks((long)(averageTimeTicks * ToleranceFactor)); var absoluteDifference = TimeSpan.FromTicks(Math.Abs(totalMismatchTimer.ElapsedTicks - almostMatchTimer.ElapsedTicks)); double actualFactor = (double)absoluteDifference.Ticks / averageTimeTicks; Assert.IsTrue(absoluteDifference <= tolerableDifference, "A total mismatch took {0} but a near match took {1}, which is too different to be indistinguishable. The tolerable difference is {2} but the actual difference is {3}. This represents a difference of {4}%, beyond the tolerated {5}%.", totalMismatchTimer.Elapsed, almostMatchTimer.Elapsed, tolerableDifference, absoluteDifference, Math.Round(actualFactor * 100), Math.Round(ToleranceFactor * 100)); Console.WriteLine("A total mismatch took {0} and a near match took {1}. The tolerable difference is {2}, and the actual difference is {3}. This represents a difference of {4}%, within the tolerated {5}%.", totalMismatchTimer.Elapsed, almostMatchTimer.Elapsed, tolerableDifference, absoluteDifference, Math.Round(actualFactor * 100), Math.Round(ToleranceFactor * 100)); Console.WriteLine("The equality test execution time difference was only {0}%, within the tolerable {1}%", Math.Round(100 * actualFactor), Math.Round(ToleranceFactor * 100)); } /// /// Verifies that the time-independent string equality check works for a given pair of strings. /// /// The first value. /// The second value. private void EqualsConstantTimeHelper(string value1, string value2) { bool expected = string.Equals(value1, value2, StringComparison.Ordinal); Assert.AreEqual(expected, MessagingUtilities.EqualsConstantTime(value1, value2)); Assert.AreEqual(expected, MessagingUtilities.EqualsConstantTime(value2, value1)); } } }