QueryString Correlation: WebTest Plug-in
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using Microsoft.VisualStudio.TestTools.WebTesting;
using Microsoft.VisualStudio.TestTools.WebTesting.Rules;
namespace WhidbeyCorrelation
{
public class CorrelationPlugin : WebTestPlugin
{
public override void PreWebTest(object sender, PreWebTestEventArgs e)
{
webtest = new XmlDocument();
webtest.Load(e.WebTest.Name + ".webtest");
//Wire up a PreRequest handler here in the Test Plugin so we don't
//need to use multiple plugins
e.WebTest.PreRequest += new EventHandler<PreRequestEventArgs>(PreRequest);
}
void PreRequest(object sender, PreRequestEventArgs e)
{
if (!IsValidRequestForCorrelation(e))
{
curReqIndex++;
return;
}
string RequestUrl = e.Request.Url.ToLower();
RequestUrl = RequestUrl.Substring(RequestUrl.LastIndexOf('/') + 1);
//Used to store info on embedded querystring parameters
//that are found during parsing
Dictionary<string, Dictionary<string, string>> linkQsps =
new Dictionary<string, Dictionary<string,string>>();
//Search the text of the Last Response for the RequestUrl
//and try to parse any querystring parameters out.
ParseLastResponse(e, linkQsps, RequestUrl);
//Present each querystring parameter value in the current
//WebTestRequest that has a different value embedded in the
//WebTest's LastResponse to the User and ask if they would
//like to use the embedded value.
QueryUserWithCorrelations(e, linkQsps, RequestUrl);
curReqIndex++;
}
public override void PostWebTest(object sender, PostWebTestEventArgs e)
{
if (userMadeChanges == true)
{
DialogResult res = MessageBox.Show("Would you like to update " +
"your Web Test to include extraction rules and bindings " +
"to automatically perform these correlations? If you " +
"select yes a backup of this webtest and the updated " +
"version of the webtest will be created in " +
e.WebTest.Context["$TestDeploymentDir"].ToString(),
"Correlation Plugin", MessageBoxButtons.YesNo);
if (res == DialogResult.Yes)
{
//Remove this Plugin from the webtest
XmlNode TestCase = webtest.SelectSingleNode("//TestCase");
TestCase.Attributes["TestCaseCallbackClass"].Value = "";
File.Copy(e.WebTest.Name + ".webtest", e.WebTest.Name +
"_bkp.webtest");
webtest.Save(e.WebTest.Name + ".webtest");
}
}
}
private bool IsValidRequestForCorrelation(PreRequestEventArgs e)
{
//Return if this is the first request
if (e.WebTest.LastResponse == null)
{
return false;
}
//Handle url's like "https://abcCompany/site/"
if (e.Request.Url.EndsWith("/"))
{
return false;
}
string RequestUrl = e.Request.Url.Substring(e.Request.Url.LastIndexOf('/') + 1);
//Handle url's like "https://abcCompany/site"
if (!RequestUrl.Contains("."))
{
return false;
}
return true;
}
private void ParseLastResponse(PreRequestEventArgs e, Dictionary<string,
Dictionary<string, string>> linkQsps, string RequestUrl)
{
string responseText = e.WebTest.LastResponse.BodyString;
string[] responseLines = responseText.Split('\n');
int urlInstance = 0;
int position = responseText.ToLower().IndexOf(RequestUrl, 0);
while (position >= 0)
{
if (responseText[position + RequestUrl.Length] == '?')
{
position = position + RequestUrl.Length;
int startIndex = ++position;
while (!endOfQuerystring.Contains(responseText[position]))
{
position++;
}
string querystring = responseText.Substring(startIndex, position - startIndex);
string[] name_value = querystring.Split('&');
//If the querystring parameter names don't match up, continue to the next embedded url
bool skipThisUrl = false;
if (name_value.Length != e.Request.QueryStringParameters.Count)
{
skipThisUrl = true;
}
else
{
foreach (string n_v in name_value)
{
if (!e.Request.QueryStringParameters.Contains(n_v.Substring(0,
n_v.IndexOf('=')).Replace("amp;", "")))
{
skipThisUrl = true;
break;
}
}
}
if (skipThisUrl == true)
{
position = responseText.ToLower().IndexOf(RequestUrl, position);
urlInstance++;
continue;
}
//Continue processing this embedded url one parameter at a time
foreach (string n_v in name_value)
{
string[] tmp = n_v.Split('=');
if (tmp.Length != 2)
continue;
string name = tmp[0].Replace("amp;", "");
string value = tmp[1];
if (!linkQsps.ContainsKey(name))
{
linkQsps.Add(name, new Dictionary<string, string>());
}
if (!linkQsps[name].ContainsKey(value))
{
foreach (QueryStringParameter qsp in e.Request.QueryStringParameters)
{
if (qsp.Name.ToLower() == name.ToLower() && qsp.Value != value)
{
//Figure out the line number and get the line of text in the response
//in which this embedded querystring name/value appears so that it
//can be presented to the user in the dialog later.
string infoForUser = null;
int responseIndex = 0;
for (int i = 0; i < responseLines.Length; i++)
{
//If Param1=a and Param1=ab both were to appear in response then
//using the call to Contains below could produce the wrong line
//number and line text, preventing this from happening by checking
//the index.
responseIndex += responseLines[i].Length;
if (responseIndex < startIndex)
{
continue;
}
if (responseLines[i].Contains(n_v))
{
//{0}, portion below will not be displayed to the user in the dialog
//it is there to give the code that creates the extraction rules on the
//webtest information it needs.
infoForUser = String.Format("{0},Line {1}: {2}", urlInstance.ToString(),
i.ToString(), responseLines[i]);
break;
}
}
linkQsps[name].Add(value, infoForUser);
break;
}
}
}
}
}
position = responseText.ToLower().IndexOf(RequestUrl, position);
urlInstance++;
} //while(position > 0)
}
private void QueryUserWithCorrelations(PreRequestEventArgs e, Dictionary<string,
Dictionary<string, string>> linkQsps, string RequestUrl)
{
foreach (QueryStringParameter qsp in e.Request.QueryStringParameters)
{
if (linkQsps.ContainsKey(qsp.Name) && linkQsps[qsp.Name].Count > 0)
{
//In cases where there are multiple values embedded in the last response that
//differ from the value currently in use by the web test a dialog will be displayed
//for each one. This isn't ideal as far as user interface goes but the reason it is
//done this way is that to bring up anything more sophisticated than a MessageBox
//would require a multithreaded approach (this code does not run in a STA thread)
//and that is beyond the scope of this sample plugin.
foreach (string embeddedValue in linkQsps[qsp.Name].Keys)
{
#region Dialog Message Text
string msgTxt = "The value of a querystring parameter on the request " +
"that is about to execute differs from a querystring parameter that " +
"is embedded in the last response text recieved during this web test. " +
"Would you like the Correlation Plugin to automatically update this for " +
"you?\r\n\r\n";
string info = linkQsps[qsp.Name][embeddedValue];
//extract instance from info string
int instance = Int32.Parse(info.Substring(0, info.IndexOf(',')));
//remove instance from info string for presentation to user
info = info.Substring(info.IndexOf(',') + 1, info.Length - info.IndexOf(',') - 2);
msgTxt += String.Format("Url: {0}\r\n\r\nQuerystring Parameter Name: {1}\r\n\r\n" +
"Current Value in WebTest: {2}\r\n\r\nValue found in last response from server " +
"during this execution: {3}\r\n\r\nWhere Found: {4}\r\n\r\n\r\n",
e.Request.Url, qsp.Name, qsp.Value, embeddedValue, info);
//Additional information is needed in the dialog if there are more than one instances of
//the url and querystring parameter with different values from the one currently used
if (linkQsps[qsp.Name].Count > 1)
{
msgTxt += "Note: Multiple instances of this querystring parameter were found with " +
"different values in the response text, only select Yes to this dialog if this " +
"is the instance you want your test to extract and bind to. The current instance " +
"is the one listed next to \"Value found in last response:\" above and also " +
"surrounded by *'s in the list of all instances found. Next to each instance " +
"listed below is the line of code where it was found.\r\n\r\nAll Instances Found:\r\n";
foreach (string val in linkQsps[qsp.Name].Keys)
{
//extract the instance data from the entry
info = linkQsps[qsp.Name][val];
info = linkQsps[qsp.Name][val].Substring(info.IndexOf(','), info.Length - info.IndexOf(','));
msgTxt = String.Concat((val == embeddedValue ? "*" + val + "*" : val) + " --> " + info, "\r\n");
}
msgTxt += "\r\n";
}
#endregion
DialogResult res = MessageBox.Show(msgTxt, "Correlation Plugin", MessageBoxButtons.YesNo);
if (res == DialogResult.Yes)
{
userMadeChanges = true;
//update value in current execution
qsp.Value = embeddedValue;
//update webtest object with extraction and binding to save off
//later if user decides to when the test has completed
#region Add the extraction rule
//plugin always uses the RawResponseText version of the extraction rule
XmlElement ExtractionRules = webtest.CreateElement("ExtractionRules");
XmlElement ExtractionRule = webtest.CreateElement("ExtractionRule");
ExtractionRule.Attributes.Append(webtest.CreateAttribute("Classname"));
ExtractionRule.Attributes["Classname"].Value =
"WhidbeyCorrelation.DynamicQueryStringExtraction_RawResponseText, " +
"WhidbeyCorrelation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
ExtractionRule.Attributes.Append(webtest.CreateAttribute("VariableName"));
ExtractionRule.Attributes["VariableName"].Value = "DynamicQuerystringExtraction_" +
extractionRuleInstance.ToString();
XmlElement RuleParameters = webtest.CreateElement("RuleParameters");
XmlElement RuleParameter0 = webtest.CreateElement("RuleParameter");
RuleParameter0.Attributes.Append(webtest.CreateAttribute("Name"));
RuleParameter0.Attributes["Name"].Value = "Url";
RuleParameter0.Attributes.Append(webtest.CreateAttribute("Value"));
RuleParameter0.Attributes["Value"].Value = RequestUrl;
XmlElement RuleParameter1 = webtest.CreateElement("RuleParameter");
RuleParameter1.Attributes.Append(webtest.CreateAttribute("Name"));
RuleParameter1.Attributes["Name"].Value = "ParameterName";
RuleParameter1.Attributes.Append(webtest.CreateAttribute("Value"));
RuleParameter1.Attributes["Value"].Value = qsp.Name;
XmlElement RuleParameter2 = webtest.CreateElement("RuleParameter");
RuleParameter2.Attributes.Append(webtest.CreateAttribute("Name"));
RuleParameter2.Attributes["Name"].Value = "Instance";
RuleParameter2.Attributes.Append(webtest.CreateAttribute("Value"));
RuleParameter2.Attributes["Value"].Value = instance.ToString();
XmlElement RuleParameter3 = webtest.CreateElement("RuleParameter");
RuleParameter3.Attributes.Append(webtest.CreateAttribute("Name"));
RuleParameter3.Attributes["Name"].Value = "AutomaticCorrelation";
RuleParameter3.Attributes.Append(webtest.CreateAttribute("Value"));
RuleParameter3.Attributes["Value"].Value = "False";
XmlElement RuleParameter4 = webtest.CreateElement("RuleParameter");
RuleParameter4.Attributes.Append(webtest.CreateAttribute("Name"));
RuleParameter4.Attributes["Name"].Value = "CorrelateNextRequestOnly";
RuleParameter4.Attributes.Append(webtest.CreateAttribute("Value"));
RuleParameter4.Attributes["Value"].Value = "False";
RuleParameters.AppendChild(RuleParameter0);
RuleParameters.AppendChild(RuleParameter1);
RuleParameters.AppendChild(RuleParameter2);
RuleParameters.AppendChild(RuleParameter3);
RuleParameters.AppendChild(RuleParameter4);
ExtractionRule.AppendChild(RuleParameters);
ExtractionRules.AppendChild(ExtractionRule);
//Determine if this request has existing extraction rules already, if it does then add our
//ExtractionRule element to the existing ExtractionRules element, otherwise add the ExtractionRules
//element to the request
if (webtest.SelectNodes("//TestCase/Items/Request[" +
curReqIndex.ToString() + "]/ExtractionRules").Count > 0)
{
//Note: xpath uses 1-based indexing so using curReqIndex
//actually grabs the last request not the current one
XmlNode n = webtest.SelectSingleNode("//TestCase/Items/Request[" +
curReqIndex.ToString() + "]/ExtractionRules");
n.AppendChild(ExtractionRule);
}
else
{
//Note: xpath uses 1-based indexing so using curReqIndex
//actually grabs the last request not the current one
XmlNode n = webtest.SelectSingleNode("//TestCase/Items/Request[" +
curReqIndex.ToString() + "]");
n.AppendChild(ExtractionRules);
}
#endregion
#region Add the querystring binding
int curReqXpath = curReqIndex + 1;
webtest.SelectSingleNode("//TestCase/Items/Request[" +
curReqXpath.ToString() + "]/QueryStringParameters/QueryStringParameter[@Name='" +
qsp.Name + "']").Attributes["Value"].Value = "{{DynamicQuerystringExtraction_" +
extractionRuleInstance.ToString() + "}}";
#endregion
extractionRuleInstance++;
break;
}
}
}
}
}
XmlDocument webtest;
int extractionRuleInstance = 0;
int curReqIndex = 0;
bool userMadeChanges = false;
//List of characters that would indicate a querystring has ended
List<char> endOfQuerystring = new List<char>(new char[] { ' ', '\r', '\n', '\'', '\"', '\t' });
}
}
Comments
- Anonymous
April 02, 2007
Overview: While creating WebTests for your site one problem you may encounter is in dealing with dynamic