-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial commit, as downloaded with no changes
- Loading branch information
0 parents
commit 2d9ed6a
Showing
34 changed files
with
10,142 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 10.00 | ||
# Visual Studio 2008 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyMvcApplication", "MyMvcApplication\MyMvcApplication.csproj", "{DA471BE6-C786-4D16-A390-3111F9E86B66}" | ||
EndProject | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyMvcApplication.Tests", "MyMvcApplication.Tests\MyMvcApplication.Tests.csproj", "{337FEBC1-F001-444B-BF72-A4380F5EA658}" | ||
EndProject | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcIntegrationTestFramework", "MvcIntegrationTestFramework\MvcIntegrationTestFramework.csproj", "{4036AFF9-698B-463D-BE26-05D2CE3E2ABD}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{DA471BE6-C786-4D16-A390-3111F9E86B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{DA471BE6-C786-4D16-A390-3111F9E86B66}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{DA471BE6-C786-4D16-A390-3111F9E86B66}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{DA471BE6-C786-4D16-A390-3111F9E86B66}.Release|Any CPU.Build.0 = Release|Any CPU | ||
{337FEBC1-F001-444B-BF72-A4380F5EA658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{337FEBC1-F001-444B-BF72-A4380F5EA658}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{337FEBC1-F001-444B-BF72-A4380F5EA658}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{337FEBC1-F001-444B-BF72-A4380F5EA658}.Release|Any CPU.Build.0 = Release|Any CPU | ||
{4036AFF9-698B-463D-BE26-05D2CE3E2ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{4036AFF9-698B-463D-BE26-05D2CE3E2ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{4036AFF9-698B-463D-BE26-05D2CE3E2ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{4036AFF9-698B-463D-BE26-05D2CE3E2ABD}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
using System; | ||
using System.Collections.Specialized; | ||
using System.IO; | ||
using System.Web; | ||
using System.Web.Mvc; | ||
using System.Web.SessionState; | ||
using MvcIntegrationTestFramework.Interception; | ||
|
||
namespace MvcIntegrationTestFramework.Browsing | ||
{ | ||
public class BrowsingSession | ||
{ | ||
public HttpSessionState Session { get; private set; } | ||
public HttpCookieCollection Cookies { get; private set; } | ||
|
||
public BrowsingSession() | ||
{ | ||
Cookies = new HttpCookieCollection(); | ||
} | ||
|
||
public RequestResult ProcessRequest(string url) | ||
{ | ||
return ProcessRequest(url, HttpVerbs.Get, null); | ||
} | ||
|
||
public RequestResult ProcessRequest(string url, HttpVerbs httpVerb, NameValueCollection formValues) | ||
{ | ||
return ProcessRequest(url, httpVerb, formValues, null); | ||
} | ||
|
||
public RequestResult ProcessRequest(string url, HttpVerbs httpVerb, NameValueCollection formValues, NameValueCollection headers) | ||
{ | ||
if (url == null) throw new ArgumentNullException("url"); | ||
|
||
// Fix up URLs that incorrectly start with / or ~/ | ||
if (url.StartsWith("~/")) | ||
url = url.Substring(2); | ||
else if(url.StartsWith("/")) | ||
url = url.Substring(1); | ||
|
||
// Parse out the querystring if provided | ||
string query = ""; | ||
int querySeparatorIndex = url.IndexOf("?"); | ||
if (querySeparatorIndex >= 0) { | ||
query = url.Substring(querySeparatorIndex + 1); | ||
url = url.Substring(0, querySeparatorIndex); | ||
} | ||
|
||
// Perform the request | ||
LastRequestData.Reset(); | ||
var output = new StringWriter(); | ||
string httpVerbName = httpVerb.ToString().ToLower(); | ||
var workerRequest = new SimulatedWorkerRequest(url, query, output, Cookies, httpVerbName, formValues, headers); | ||
HttpRuntime.ProcessRequest(workerRequest); | ||
|
||
// Capture the output | ||
AddAnyNewCookiesToCookieCollection(); | ||
Session = LastRequestData.HttpSessionState; | ||
return new RequestResult | ||
{ | ||
ResponseText = output.ToString(), | ||
ActionExecutedContext = LastRequestData.ActionExecutedContext, | ||
ResultExecutedContext = LastRequestData.ResultExecutedContext, | ||
Response = LastRequestData.Response, | ||
}; | ||
} | ||
|
||
private void AddAnyNewCookiesToCookieCollection() | ||
{ | ||
if(LastRequestData.Response == null) | ||
return; | ||
|
||
HttpCookieCollection lastResponseCookies = LastRequestData.Response.Cookies; | ||
if(lastResponseCookies == null) | ||
return; | ||
|
||
foreach (string cookieName in lastResponseCookies) { | ||
HttpCookie cookie = lastResponseCookies[cookieName]; | ||
if (Cookies[cookieName] != null) | ||
Cookies.Remove(cookieName); | ||
if((cookie.Expires == default(DateTime)) || (cookie.Expires > DateTime.Now)) | ||
Cookies.Add(cookie); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace MvcIntegrationTestFramework.Browsing | ||
{ | ||
public static class MvcUtils | ||
{ | ||
public static string ExtractAntiForgeryToken(string htmlResponseText) | ||
{ | ||
if (htmlResponseText == null) throw new ArgumentNullException("htmlResponseText"); | ||
|
||
Match match = Regex.Match(htmlResponseText, @"\<input name=""__RequestVerificationToken"" type=""hidden"" value=""([^""]+)"" \/\>"); | ||
return match.Success ? match.Groups[1].Captures[0].Value : null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System.Web; | ||
using System.Web.Mvc; | ||
|
||
namespace MvcIntegrationTestFramework.Browsing | ||
{ | ||
/// <summary> | ||
/// Represents the result of a simulated request | ||
/// </summary> | ||
public class RequestResult | ||
{ | ||
public HttpResponse Response { get; set; } | ||
public string ResponseText { get; set; } | ||
public ActionExecutedContext ActionExecutedContext { get; set; } | ||
public ResultExecutedContext ResultExecutedContext { get; set; } | ||
} | ||
} |
88 changes: 88 additions & 0 deletions
88
MvcIntegrationTestFramework/Browsing/SimulatedWorkerRequest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
using System; | ||
using System.Collections.Specialized; | ||
using System.IO; | ||
using System.Text; | ||
using System.Web; | ||
using System.Web.Hosting; | ||
using System.Linq; | ||
|
||
namespace MvcIntegrationTestFramework.Browsing | ||
{ | ||
internal class SimulatedWorkerRequest : SimpleWorkerRequest | ||
{ | ||
private HttpCookieCollection cookies; | ||
private readonly string httpVerbName; | ||
private readonly NameValueCollection formValues; | ||
private readonly NameValueCollection headers; | ||
|
||
public SimulatedWorkerRequest(string page, string query, TextWriter output, HttpCookieCollection cookies, string httpVerbName, NameValueCollection formValues, NameValueCollection headers) | ||
: base(page, query, output) | ||
{ | ||
this.cookies = cookies; | ||
this.httpVerbName = httpVerbName; | ||
this.formValues = formValues; | ||
this.headers = headers; | ||
} | ||
|
||
public override string GetHttpVerbName() | ||
{ | ||
return httpVerbName; | ||
} | ||
|
||
public override string GetKnownRequestHeader(int index) | ||
{ | ||
// Override "Content-Type" header for POST requests, otherwise ASP.NET won't read the Form collection | ||
if (index == 12) | ||
if (string.Equals(httpVerbName, "post", StringComparison.OrdinalIgnoreCase)) | ||
return "application/x-www-form-urlencoded"; | ||
|
||
switch (index) { | ||
case 0x19: | ||
return MakeCookieHeader(); | ||
default: | ||
if (headers == null) | ||
return null; | ||
return headers[GetKnownRequestHeaderName(index)]; | ||
} | ||
} | ||
|
||
public override string GetUnknownRequestHeader(string name) | ||
{ | ||
if(headers == null) | ||
return null; | ||
return headers[name]; | ||
} | ||
|
||
public override string[][] GetUnknownRequestHeaders() | ||
{ | ||
if (headers == null) | ||
return null; | ||
var unknownHeaders = from key in headers.Keys.Cast<string>() | ||
let knownRequestHeaderIndex = GetKnownRequestHeaderIndex(key) | ||
where knownRequestHeaderIndex < 0 | ||
select new[] { key, headers[key] }; | ||
return unknownHeaders.ToArray(); | ||
} | ||
|
||
public override byte[] GetPreloadedEntityBody() | ||
{ | ||
if(formValues == null) | ||
return base.GetPreloadedEntityBody(); | ||
|
||
var sb = new StringBuilder(); | ||
foreach (string key in formValues) | ||
sb.AppendFormat("{0}={1}&", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(formValues[key])); | ||
return Encoding.UTF8.GetBytes(sb.ToString()); | ||
} | ||
|
||
private string MakeCookieHeader() | ||
{ | ||
if((cookies == null) || (cookies.Count == 0)) | ||
return null; | ||
var sb = new StringBuilder(); | ||
foreach (string cookieName in cookies) | ||
sb.AppendFormat("{0}={1};", cookieName, cookies[cookieName].Value); | ||
return sb.ToString(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
using System; | ||
using MvcIntegrationTestFramework.Browsing; | ||
|
||
namespace MvcIntegrationTestFramework.Hosting | ||
{ | ||
/// <summary> | ||
/// Simply provides a remoting gateway to execute code within the ASP.NET-hosting appdomain | ||
/// </summary> | ||
internal class AppDomainProxy : MarshalByRefObject | ||
{ | ||
public void RunCodeInAppDomain(Action codeToRun) | ||
{ | ||
codeToRun(); | ||
} | ||
|
||
public void RunBrowsingSessionInAppDomain(SerializableDelegate<Action<BrowsingSession>> script) | ||
{ | ||
var browsingSession = new BrowsingSession(); | ||
script.Delegate(browsingSession); | ||
} | ||
|
||
public override object InitializeLifetimeService() | ||
{ | ||
return null; // Tells .NET not to expire this remoting object | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Web; | ||
using System.Web.Hosting; | ||
using System.Web.Mvc; | ||
using MvcIntegrationTestFramework.Browsing; | ||
using MvcIntegrationTestFramework.Interception; | ||
|
||
namespace MvcIntegrationTestFramework.Hosting | ||
{ | ||
/// <summary> | ||
/// Hosts an ASP.NET application within an ASP.NET-enabled .NET appdomain | ||
/// and provides methods for executing test code within that appdomain | ||
/// </summary> | ||
public class AppHost | ||
{ | ||
private readonly AppDomainProxy appDomainProxy; // The gateway to the ASP.NET-enabled .NET appdomain | ||
|
||
public AppHost(string appPhysicalDirectory) : this(appPhysicalDirectory, "/") | ||
{ | ||
} | ||
|
||
public AppHost(string appPhysicalDirectory, string virtualDirectory) | ||
{ | ||
try { | ||
appDomainProxy = (AppDomainProxy) ApplicationHost.CreateApplicationHost(typeof (AppDomainProxy), virtualDirectory, appPhysicalDirectory); | ||
} catch(FileNotFoundException ex) { | ||
if((ex.Message != null) && ex.Message.Contains("MvcIntegrationTestFramework")) | ||
throw new InvalidOperationException("Could not load MvcIntegrationTestFramework.dll within a bin directory under " + appPhysicalDirectory + ". Is this the path to your ASP.NET MVC application, and have you set up a post-build event to copy your test assemblies and their dependencies to this folder? See the demo project for an example."); | ||
throw; | ||
} | ||
|
||
appDomainProxy.RunCodeInAppDomain(() => { | ||
InitializeApplication(); | ||
AttachTestControllerDescriptorsForAllControllers(); | ||
LastRequestData.Reset(); | ||
}); | ||
} | ||
|
||
public void SimulateBrowsingSession(Action<BrowsingSession> testScript) | ||
{ | ||
var serializableDelegate = new SerializableDelegate<Action<BrowsingSession>>(testScript); | ||
appDomainProxy.RunBrowsingSessionInAppDomain(serializableDelegate); | ||
} | ||
|
||
#region Initializing app & interceptors | ||
private static void InitializeApplication() | ||
{ | ||
var appInstance = GetApplicationInstance(); | ||
appInstance.PostRequestHandlerExecute += delegate { | ||
// Collect references to context objects that would otherwise be lost | ||
// when the request is completed | ||
if(LastRequestData.HttpSessionState == null) | ||
LastRequestData.HttpSessionState = HttpContext.Current.Session; | ||
if (LastRequestData.Response == null) | ||
LastRequestData.Response = HttpContext.Current.Response; | ||
}; | ||
RefreshEventsList(appInstance); | ||
|
||
RecycleApplicationInstance(appInstance); | ||
} | ||
|
||
private static void AttachTestControllerDescriptorsForAllControllers() | ||
{ | ||
var allControllerTypes = from assembly in AppDomain.CurrentDomain.GetAssemblies() | ||
from type in assembly.GetTypes() | ||
where typeof (IController).IsAssignableFrom(type) | ||
select type; | ||
foreach (var controllerType in allControllerTypes) | ||
InterceptionFilter.AssociateWithControllerType(controllerType); | ||
} | ||
#endregion | ||
|
||
#region Reflection hacks | ||
private static readonly MethodInfo getApplicationInstanceMethod; | ||
private static readonly MethodInfo recycleApplicationInstanceMethod; | ||
|
||
static AppHost() | ||
{ | ||
// Get references to some MethodInfos we'll need to use later to bypass nonpublic access restrictions | ||
var httpApplicationFactory = typeof(HttpContext).Assembly.GetType("System.Web.HttpApplicationFactory", true); | ||
getApplicationInstanceMethod = httpApplicationFactory.GetMethod("GetApplicationInstance", BindingFlags.Static | BindingFlags.NonPublic); | ||
recycleApplicationInstanceMethod = httpApplicationFactory.GetMethod("RecycleApplicationInstance", BindingFlags.Static | BindingFlags.NonPublic); | ||
} | ||
|
||
private static HttpApplication GetApplicationInstance() | ||
{ | ||
var writer = new StringWriter(); | ||
var workerRequest = new SimpleWorkerRequest("", "", writer); | ||
var httpContext = new HttpContext(workerRequest); | ||
return (HttpApplication)getApplicationInstanceMethod.Invoke(null, new object[] { httpContext }); | ||
} | ||
|
||
private static void RecycleApplicationInstance(HttpApplication appInstance) | ||
{ | ||
recycleApplicationInstanceMethod.Invoke(null, new object[] { appInstance }); | ||
} | ||
|
||
private static void RefreshEventsList(HttpApplication appInstance) | ||
{ | ||
object stepManager = typeof (HttpApplication).GetField("_stepManager", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(appInstance); | ||
object resumeStepsWaitCallback = typeof(HttpApplication).GetField("_resumeStepsWaitCallback", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(appInstance); | ||
var buildStepsMethod = stepManager.GetType().GetMethod("BuildSteps", BindingFlags.NonPublic | BindingFlags.Instance); | ||
buildStepsMethod.Invoke(stepManager, new[] { resumeStepsWaitCallback }); | ||
} | ||
|
||
#endregion | ||
} | ||
} |
Oops, something went wrong.