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' });

    }

}