(... or Validators fail to fire in ASP.NET CompositeControl)
Recently, in a tale of WebTests, IIS Application Pools and assembly load exceptions
I discussed how I'd put together a suite of VSTS web tests and, at the end of the post I promised "Next, how my WebTests accidentally highlighted a security vulnerability on this very blog!"
The issue involved the Captcha control that we use when users leave a comment:
The control was written as a reusable CompositeControl
that can be dropped on to any page and the page author only has to check Page.IsValid
to be confident that the Captcha has been completed successfully.
And this worked fine whenever I tested it from a browser... but I found that it didn't work through the VSTS web test engine! Users could leave comments and just type nonsense into the Captcha. No good!
It took a long time to get to the bottom of this (and some very useful input from my colleague Simon Ince
). In hindsight, the explanation makes it appear bleedingly obvious but it really wasn't at the time.
It transpires that the page had changed structure ever-so-slightly since the webtest was recorded. Here's Fiddler's view of the form post when a comment is submitted:
You can see the CaptchaControl's value being submitted with the field name ctl00$MainPlaceHolder$_commentEditor$ctl00$_captchaControl$captcha
. As I mentioned previously, the page had changed since the WebTest was recorded and the webtest was submitting a field name of something like ctl01$MainPlaceHolder$_commentEditor$ctl01$_captchaControl$captcha
. I know, blink and you'd miss it - I did.
Of course, the problem here is that because there is no value specified for our actual CaptchControl - the darned thing never gets instantiated and thus nor do it's Validators (meaning Page.IsValued == true, no!).
Fortunately, this is easy to fix. We need to instruct our composite control to always instantiate, even when it isn't specified in the postback. We can do this in two steps:
First we need to implement IPostBackDataHandler interface:
public class CaptchaControl : CompositeControl, IPostBackDataHandler
Which requires only two methods to be implemented.
public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
public void RaisePostDataChangedEvent()
Note that we call EnsureChildControls inside LoadPostData which makes sure that our Validators are created appropriately. The next step is register the control with the page so that it is instantiated even if it's field name isn't included in the PostBack. That's easy, inside my control's OnInit method we call Page.RegisterRequiresPostBack(this):
protected override void OnInit(EventArgs e)
This all sounds blatantly obvious as I write it now but it had me stumped for a while. It serves to demonstrate how a variety of testing mechanisms can serve to identify issues you wouldn't have conceived. I'll certainly be using web tests and configuring them manually to explore potential issues like this in future.