OWIN Self-Hosted Test Server for Integration Testing of OData and Web API

A co-worker of mine and I were recently given a task to perform integration testing on OData and Web API services. Traditionally, one might mock the web requests and responses, but by using the TestServer found in Microsoft.Owin.Testing namespace, we can start an in-memory HTTP server for doing full integration tests. You can get the NuGet package here.

To start create a new Unit Testing project with MS Test and a new ASP.NET MVC / Web API project. In the ASP.NET MVC / Web API project install Web API 2.2 and Web API 2.2 for OData v4.0 and OData v1-3. For the unit test project, install Web API 2.2, Web API 2.2 for OData v4.0 and OData v1-3, Web API 2.2 OWIN Self Host, Web API 2.2 OWIN, and Microsoft.Owin.Testing. Below is a sample of a unit/integration test for setting up an OWIN test server for in-memory integration testing:

namespace SelfHosting.Test
{
    using Microsoft.Owin.Testing;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Owin;
    using SelfHosting.Test.Models;
    using System;
    using System.Linq;
    using System.Net.Http;
    using System.Threading.Tasks;
    using System.Web.Http;
    using System.Web.Http.Dispatcher;
    using System.Web.OData.Builder;
    using System.Web.OData.Extensions;
    using WebApp.Models;

    [TestClass]
    public class SelfHostingTest
    {
        protected TestServer server;

        [TestInitialize]
        public void Setup()
        {
            server = TestServer.Create(app =>
            {
                HttpConfiguration config = new HttpConfiguration();
                WebAppFacade.WebApiConfig.Register(config);
                app.UseWebApi(config);
            });
        }

        [TestCleanup]
        public void TearDown()
        {
            if (server != null)
                server.Dispose();
        }

        [TestMethod]
        public async Task TestODataMetaData()
        {
            HttpResponseMessage response = await server.CreateRequest("/odata/?$metadata").GetAsync();

            var result = await response.Content.ReadAsAsync<ODataMetaData>();

            Assert.IsTrue(result.value.Count > 0, "Unable to obtain meta data");
        }

        [TestMethod]
        public async Task TestWebApi()
        {
            HttpResponseMessage response = await server.CreateRequest("/api/values").GetAsync();

            var result = await response.Content.ReadAsStringAsync();

            Assert.AreEqual("\"Hello from foreign assembly!\"", result, "/api/values not configured correctly");
        }

        [TestMethod]
        public async Task TestODataPeople()
        {
            HttpResponseMessage response = await server.CreateRequest("/odata/People").GetAsync();

            var result = await response.Content.ReadAsAsync<ODataResponse<Person>>();

            Assert.AreEqual(result.value.Count, 3, "Expected 3 people to return from /odata/People");
        }
    }

}

The OData meta data is serialized into a POCO (Plain old C# object):

namespace SelfHosting.Test.Models
{
    using System.Collections.Generic;

    public class ODataMetaData
    {
        public string odatacontext { get; set; }
        public List<Value> value { get; set; }
    }

    public class Value
    {
        public string name { get; set; }
        public string kind { get; set; }
        public string url { get; set; }
    }
}

By using a generic ODataResponse class, we can deserialize our OData response into any POCO:

namespace SelfHosting.Test.Models
{
    using System.Collections.Generic;

    public class ODataResponse<T>
        where T : class, new()
    {
        public string odatacontext { get; set; }
        public List<T> value { get; set; }

        public ODataResponse()
        {

        }
    }
}
The beauty about using the TestServer is that it is self-contained and the HTTP server is inaccessible outside of the process. Once the tests complete, the server is shutdown. The WebApiConfig registered with the TestServer determines which controllers and routes to load for testing. No production code needs to be changed in order to test existing Web API and OData controllers. The only problem that I have found is that attribute routes don't seem to register correctly. Perhaps I have not found the correct method of registering the attribute routes for the TestServer.
Here is the Visual Studio 2013 solution with both a web project and a unit testing project:

SelfHostingUnitTest.zip (1.39 mb)

TFS Conflicts Take Source Resolve

7. October 2014 14:17 by Cameron in Programming  //  Tags: , , ,   //   Comments

Today, I had to merge a bunch of items from one branch to another but had about 350+ conflicts and manually accepting the Take Source wasn't going to cut it. I found that if you open up the Visual Studio Command Prompt, you have access to the tf tool which allows you to work with TFS from a commandline interface. After changing to my solution's directory, the following command allowed me to merge my changes automatically by resolving to take the source branch:

tf resolve /auto:TakeTheirs

After about a minute or two, I was able to see that all conflicts were resolved and I checked in my changes. This is a huge time saver as it would have taken me several magnitudes longer to resolve them manually.

Life Update and Gamer Footprint Update

7. August 2014 09:03 by Cameron in   //  Tags: , , , , , , , ,   //   Comments

Hey guys, I know it's been a while since I've written a blog post. I've been busy with wedding planning and other life events! I'm still alive and kicking!

Gamer Footprint (GFP) continues development in the spare time that I have. It's making slow but sure progress. I've moved Gamer Footprint to use a single page application (SPA) approach. The previous iterations of GFP were based on strict ASP.NET MVC, but as of late, I've been on a single page application kick. :) The advantages to SPAs are excellent over traditional multi-page applications. With SPAs, you can load your application shell and then load/post content on demand. This gives a perceived faster and more responsive application. 

With GFP, I've been playing around with Web API 2 and Durandal. Web API 2 is Microsoft's take on RESTful API development. It's very robust and supports all types of HTTP calls as defined by the REST design principles. By using Durandal, I can use RequireJS, Knockout.js, and jQuery to build rich single page applications. This proves to be a good choice for the development of GFP because I already have experience in RequireJS, Knockout, and jQuery. If I had chosen to use AngularJS, it would have required to learn a completely new framework. While some might not worry about that, I've already been using Knockout in some projects at my day job, so naturally, it made sense to learn Durandal.

Visual Studio Update 3 and TypeScript external module compilation

7. August 2014 08:54 by Cameron in TypeScript  //  Tags: , , , , , , , ,   //   Comments

I recently updated to Visual Studio 2013 Update 3, but it appears as though the TypeScript compiler is a little more strict in external module compilation. If you receive an error message asking the --module flag to be set, you need to add this to your build configurations in your project:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <TypeScriptModuleKind>amd</TypeScriptModuleKind>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <TypeScriptModuleKind>amd</TypeScriptModuleKind>
  </PropertyGroup>

After adding the TypeScriptModule kind node to the project properties, the compiler can correctly compile external TypeScript modules!

Disqus now in effect for comments!

2. May 2014 14:02 by Cameron in Blog  //  Tags:   //   Comments

I don't know why it took so long to make the move to Disqus for the comment system, but I'm glad I did. There is one caveat though: all previous comments are now hidden. Moving forward, we will have much more flexibility for comments and hopefully less spam. You wouldn't believe how much spam I was getting with the standard comments system. Some spam bots were so badly written that they had malformed anchor tags embedded in their comments. It's extremely clear when comments are spam or when they're not.

Knockout Observable Array Performance Improvments

Today, I was working on a project that makes AJAX calls to an API and noticed that the application was very slow and unresponsive during the AJAX calls and ko updates. I checked that the requests weren't taking too long in Chrome's developer tools and found they were at max taking 300ms and minimum of 20ms to return. This was very odd behavior since the AJAX requests weren't blocking.

After some research, I found that when updating ko.observableArrays, you don't want to push each individual item to the array if you can help it. Each time an item is pushed to a ko.observableArray, it notifies its subscribers and depending on how many DOM elements you have on your page, that can be rather taxing. I had code like this:

$.ajax({
	url: 'api/Document/',
	method: 'GET',
	dataType: 'json',
	success: function(data) {
		Documents([])
		for(var i = 0; i < data.length; i++)
			Documents.push(data[i]);
	}
});

To fix this, you need to create temporary arrays to push your items to and then assign the ko.observableArray to the contents of the temporary array. Your code should look more like this:

$.ajax({
	url: 'api/Document/',
	method: 'GET',
	dataType: 'json',
	success: function(data) {
		var documents = [];
		for(var i = 0; i < data.length; i++)
			documents.push(data[i]);	

		Documents(documents);
	}
});

Gamer Footprint Update

4. March 2014 11:27 by Cameron in Gamer Footprint, Programming  //  Tags: , ,   //   Comments

Some of you may be wondering what is the current status of Gamer Footprint? Development is live and well! In fact, there is a slightly functional proof of concept available to test at https://www.gamerfootprint.com. I have also bought the domain www.gamrfp.com but this domain doesn't have any SSL configured yet. I mainly bought it for a shorter domain.

Please do note that the website is rapidly changing with new features several times a week and I sometimes have to clear out the database when my domain model changes or I find bugs that cause duplicate data. Therefore, the site is not 100% stable yet. I'm working very hard on making the user experience enjoyable. Some features that have been implemented are:

  • User profiles
    • The profiles are very basic right now, but I plan to implement a full timeline-like profile that includes a gamer's events such as played games or achievements/trophies
    • Your avatar is currently pulled from Gravatar and if you don't have a Gravatar, you should get one! It's a very nice service to have the same avatar across multiple websites. If no Gravatar can be found for your account email, it will display a generic image in place of your avatar.
    • If you have entered a PlayStation Network and/or an Xbox LIVE account, your profile will show your current presence on PlayStation Network and Xbox LIVE
    • Your information is safe and secured and transmitted over SSL. You only have to login once for your PSN or Xbox LIVE accounts. We don't store any passwords for accounts on our servers. I'm working on providing a method for not requiring account credentials, but you won't be able to send messages. If messaging your Xbox LIVE or PSN friends from a PC is important, you'll need to enter your account credentials.
  • Xbox LIVE games and achievements (Xbox One games and challenges support to come soon!)
  • Data APIs that directly interface with Xbox LIVE, PlayStation Network, and Steam
    • Get Xbox and Xbox One games, achievements, challenges, friends, presence and profile information
    • Get PlayStation games, trophies, friends, presence information and profile information - supports PSVITA, PS3, and PS4
    • These APIs have not yet been made public, but they should be available very soon. I will post documentation on how to request an API key and API endpoints for PSN and Xbox LIVE. I will not provide a data API for Steam since Valve provides their own and I don't want to use up my daily requests to the API with a wrapper API. There might be a way to let developers aggregate everything from all supported game networks (including Steam), but it would involve providing individual Valve issued API keys and I don't want to do that as it might be against their terms of service.
Planned features:
  • PlayStation Network games and trophies
    • The core for interfacing with PSN has been finished, but I have yet to write the data access layer and data persistence layer for this part
  • Timeline system - user level and global level
  • Message friends on Xbox LIVE and PSN from the website
  • Invite people via Xbox LIVE or PSN message
  • and much more!
Here are some preview screenshots:
 

Windows Path Environment Variable Length

10. February 2014 13:14 by Cameron in Windows  //  Tags: , , , , , ,   //   Comments

I recently started getting "Target Invocation" errors while trying to launch Visual Studio 2012 or Visual Studio 2013. This was a strange error message as not much else was given to diagnose the problem. After some extended research, I found that having an extra long PATH variable can cause Visual Studio to hang on startup. Windows limits this length to 2048 characters. Although you can still have a PATH variable longer than that, some programs like Visual Studio can't address a PATH environment variable larger than 2048 characters. A remedy to this problem is to split out your PATH variable into at least two variables. 

First, you need to open your PATH variable and copy its value into a safe spot in case it gets messed up. Then you can extract the last half of your path and create a new environment variable and call it PATH2. After your new environment variable is setup, you can reference it in your main PATH

C:\bin;C:\anotherfolder\bin;%PATH2%

Some may be asking, why would my PATH ever exceed 2048 characters? The answer is that if you're a developer and have lots of compilers, IDEs, and toolchains installed on your development machine such as Visual Studio, Net Beans, GCC, Qt, or devkitpro, your PATH can grow fairly quickly. 

If you ever exceed 2048 characters in PATH2, you can append another variable such as PATH3 or PATH4. I'm not certain if there is a real physical limit to user created environment variables, but if you experience problems, you can do as I mentioned. Note, I have a Windows 7 x64 development machine at work and I haven't seen this issue in Windows XP or Windows 8. I haven't done development on Windows XP in several years since it is now showing its age and Microsoft is discontinuing support in April 2014.

On-the-Fly Windows Impersonation with C#

6. February 2014 09:37 by Cameron in Programming  //  Tags:   //   Comments

Occasionally I need to perform tasks under another user so I created a nice wrapper class that allows me to impersonate as another user on the fly:

using System;

using System.Runtime.InteropServices;

using System.Security.Permissions;

using System.Security.Principal;

using Microsoft.Win32.SafeHandles;

using System.Runtime.ConstrainedExecution;

using System.Security;

using System.Configuration;



namespace Tinksoft.Tools

{

    /// <summary>

    /// Facilitates impersonation of a Windows User.

    /// </summary>

    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]

    public class Impersonation : IDisposable

    {

        public string Environment { get; set; }



        public string UserName { get; set; }



        public string Password { get; set; }



        public string DomainName { get; set; }



        public enum LogonType

        {

            Interactive = 2,

            Network = 3,

            Batch = 4,

            Service = 5,

            Unlock = 7,

            NetworkClearText = 8,

            NewCredentials = 9

        }



        public enum LogonProvider

        {

            Default = 0,

            WinNT35 = 1,

            WinNT40 = 2,

            WinNT50 = 3

        }



        /// <summary>

        /// Windows Token.

        /// </summary>

        private readonly SafeTokenHandle _handle;



        /// <summary>

        /// The impersonated User.

        /// </summary>

        private WindowsImpersonationContext impersonatedUser;



        public Impersonation()

        {

        }



        /// <summary>

        /// Initializes a new instance of the Impersonation class. Provides domain, user name, and password for impersonation.

        /// </summary>

        /// <param name="domainName">Domain name of the impersonated user.</param>

        /// <param name="userName">Name of the impersonated user.</param>

        /// <param name="password">Password of the impersonated user.</param>

        /// <remarks>

        /// Uses the unmanaged LogonUser function to get the user token for

        /// the specified user, domain, and password.

        /// </remarks>

        public Impersonation(AccountCredentials credentials)

        {            

            string[] splitName = WindowsIdentity.GetCurrent().Name.Split('\\');

            string name = (splitName.Length > 0) ? splitName[0] : null;



            LogonType logonType = LogonType.Interactive;

            LogonProvider logonProvider = LogonProvider.Default;



            if (name != credentials.Domain)

            {

                logonType = LogonType.NewCredentials;

                logonProvider = LogonProvider.WinNT50;

            }



            // Call LogonUser to obtain a handle to an access token.

            bool returnValue = LogonUser(

                                credentials.UserName,

                                credentials.Domain,

                                credentials.Password,

                                (int)logonType,

                                (int)logonProvider,

                                out this._handle);



            if (false == returnValue)

            {

                // Something went wrong.

                int ret = Marshal.GetLastWin32Error();

                throw new System.ComponentModel.Win32Exception(ret);

            }



            this.impersonatedUser = WindowsIdentity.Impersonate(this._handle.DangerousGetHandle());    

        }



        /// <summary>

        /// Initializes a new instance of the Impersonation class. Provide domain, user name, and password for impersonation.

        /// </summary>

        /// <param name="domainName">Domain name of the impersonated user.</param>

        /// <param name="userName">Name of the impersonated user.</param>

        /// <param name="password">Password of the impersonated user.</param>

        /// <remarks>

        /// Uses the unmanaged LogonUser function to get the user token for

        /// the specified user, domain, and password.

        /// </remarks>

        public Impersonation(string domainName, string userName, string password)

        {

            string[] splitName = WindowsIdentity.GetCurrent().Name.Split('\\');

            string name = (splitName.Length > 0) ? splitName[0] : null;



            LogonType logonType = LogonType.Interactive;

            LogonProvider logonProvider = LogonProvider.Default;



            if (name != domainName)

            {

                logonType = LogonType.NewCredentials;

                logonProvider = LogonProvider.WinNT50;

            }



            // Call LogonUser to obtain a handle to an access token.

            bool returnValue = LogonUser(

                                userName,

                                domainName,

                                password,

                                (int)logonType,

                                (int)logonProvider,

                                out this._handle);



            if (false == returnValue)

            {

                // Something went wrong.

                int ret = Marshal.GetLastWin32Error();

                throw new System.ComponentModel.Win32Exception(ret);

            }



            this.impersonatedUser = WindowsIdentity.Impersonate(this._handle.DangerousGetHandle());

        }



        [DllImport("advapi32.dll", SetLastError = true)]

        private static extern bool LogonUser(

                string lpszUsername,

                string lpszDomain,

                string lpszPassword,

                int dwLogonType,

                int dwLogonProvider,

                out SafeTokenHandle phToken);



        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]

        private static extern bool CloseHandle(IntPtr handle);



        public void Dispose()

        {

            this.impersonatedUser.Dispose();

            this._handle.Dispose();

        }



        private static string[] GetAccountInfo(string accountInfo)

        {

            return accountInfo.Split(' ');

        }

    }



    public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid

    {

        private SafeTokenHandle()

            : base(true) { }



        [DllImport("kernel32.dll")]

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]

        [SuppressUnmanagedCodeSecurity]

        [return: MarshalAs(UnmanagedType.Bool)]

        private static extern bool CloseHandle(IntPtr handle);



        protected override bool ReleaseHandle()

        {

            return CloseHandle(handle);

        }

    }

}
 

Happy New Year 2014!

6. January 2014 22:48 by Cameron in Programming  //  Tags:   //   Comments

I know I'm a few days late for posting on New Year's day, but I was away in a cabin in northern California with limited Internet. 2014 is a new year and I plan to write at least one blog post a month. Last year, I didn't write a post each month, but I don't want to become uninterested in blog writing and learning new technologies. The best action a developer can take for his/her career is learning new skills - constantly. The world of technology is forever changing and evolving and those who sit complacently will fall behind and lose their competitive edge. Each year, as new students graduate college, they will be sharper and brighter with the newest tech in the industry. Anyone who fails to embrace new technologies will not survive in the coming wave of millenials entering the workforce, eventually replacing the vast majority of the workforce. People who fail to think about the future will sadly not have a future in IT. If you're in your mid to late 30's and haven't picked up a new skill lately, it's time to do so. Time is of the essence and the future of IT lies in the hearts and minds of the millenials and their children.

Month List

Tag cloud