- Integration testing your asp .net core app with an in memory database
- Integration testing your asp .net core app dealing with anti request forgery csrf formdata and cookies (this)
This post can be considered a sequel to Setting up integration testing in Asp.Net Core. It builds upon some code provided there.
Running Integration Tests are awesome. (Be mindful though: They Are A Scam as well).
So great, you got it up and running. And you probably will run into a few (practical) things when dealing with a bit more useful scenario’s. In this case I describe visiting a page (GET) and POSTing a form. Also we deal with the case when you have set up Anti Request Forgery.
I will give you a few hints of code. For your convenience I have also put them up as Github Gists.
Disclaimer about the presented code:
I have come to this code after some investigation, but I lost track of what source lead to what piece of code. So if you are (or know) the source of pieces of code, please notify me and I will give credits where credits are due.
Now we have that out of the way, lets get started!
Context – some example code
To explain things easier, lets say you have a GET and POST action defined (for same URL). The GET delivers a view with a form; in your integration test you want to do a POST to the same URL as if the user filled in the form. (Since we’re not UI testing, we don’t care about the HTML – no need to play browser here).
In our example, lets say we have some code like this:
// GET request to awesomesauce [Route("awesomesauce")] public async TaskAwesomeSauce() { var model = new MyAwesomeModel(); return View(model); } // POST request to awesomesauce [HttpPost, Route("awesomesauce"), ValidateAntiForgeryToken] public async Task AwesomeSauce(MyAwesomeModel myAwesomeModel) { // if valid, do stuff // else... return View(myAwesomeModel); }
Your integration test would look like this:
[Collection("Integration tests collection")] public class AwesomeSauceTest : AbstractControllerIntegrationTest { public AwesomeSauceTest(TestServerFixture testServerFixture) : base(testServerFixture) { } [Fact] public async Task Visits_AwesomeSauce_And_Posts_Data() { var response = await client.GetAsync("/awesomesauce"); response.EnsureSuccessStatusCode(); // How do we do this? Send data (POST) - with anti request forgery token and all?... lets find out! //var response = await client.SendAsync(requestMessage); } }
In the test we marked our questions. How do we post data? And how do send this AntiRequestForgery token?
Lets begin with POSTing data. In a convenient world I would like to present a
Dictionary
with keys and values, then simply pass that as method BODY and let some helper method transform that into a true HttpRequest message. I made such a thing my own, it looks like this:
With the
PostRequestHelper
we can now POST data like so:
public class AwesomeSauceTest : AbstractControllerIntegrationTest { public AwesomeSauceTest(TestServerFixture testServerFixture) : base(testServerFixture) { } [Fact] public async Task Visits_AwesomeSauce_And_Posts_Data() { var response = await client.GetAsync("/awesomesauce"); response.EnsureSuccessStatusCode(); var formPostBodyData = new Dictionary{ {"Awesomesauce.Foo", "Bar"}, {"Awesomesauce.AnotherKey", "Baz"}, {"Any_Other_Form_Key", "Any_Other_Value"} }; var requestMessage = PostRequestHelper.Create("/awesomesauce", formPostBodyData); // TODO: AntiRequestForgery token var response = await client.SendAsync(requestMessage); // Assert } }
Well that is easy isn’t it?
If you paid attention you already saw a hint in the
PostRequestHelper
about a
CookiesHelper
. Although it is not needed to deal with AntiRequestForgery, it is a handy tool. I’ll explain it below.
Dealing with the AntiRequestForgery token
In general it is easy, you do a GET, in its response you receive a token. You need to extract that token and put that token on your next POST request and you’re done.
To extract the token, you can use this:
Now we can use it in our test like so:
public class AwesomeSauceTest : AbstractControllerIntegrationTest { public AwesomeSauceTest(TestServerFixture testServerFixture) : base(testServerFixture) { } [Fact] public async Task Visits_AwesomeSauce_And_Posts_Data() { var response = await client.GetAsync("/awesomesauce"); response.EnsureSuccessStatusCode(); string antiForgeryToken = await AntiForgeryHelper.ExtractAntiForgeryToken(response); var formPostBodyData = new Dictionary{ {"__RequestVerificationToken", antiForgeryToken}, // Add token {"Awesomesauce.Foo", "Bar"}, {"Awesomesauce.AnotherKey", "Baz"}, {"Any_Other_Form_Key", "Any_Other_Value"} }; var requestMessage = PostRequestHelper.Create("/awesomesauce", formPostBodyData); var response = await client.SendAsync(requestMessage); // Assert } }
And voila, you can now do a POST request which will pass the token and make the POST happen. By omitting the token you can test if your action is protected by CSRF (or using a different token). Although I would not try to test the framework itself, I would advice to have tests in place that make sure specific controller actions are protected.
Dealing with Cookies
As bonus, lets deal with Cookies. You need to deal with those probably. Sometimes you need to post the data again (as if you are a real browser). In that case to make life easier there is a method on the
PostRequestHelper
called
CreateWithCookiesFromResponse
. This basically creates a POST request, and copies over your cookies from a (previous) GET request.
The CookiesHelper looks like this:
In our example test above, we could have used it like this:
public class AwesomeSauceTest : AbstractControllerIntegrationTest { public AwesomeSauceTest(TestServerFixture testServerFixture) : base(testServerFixture) { } [Fact] public async Task Visits_AwesomeSauce_And_Posts_Data() { var response = await client.GetAsync("/awesomesauce"); // this returns cookies in response response.EnsureSuccessStatusCode(); string antiForgeryToken = await AntiForgeryHelper.ExtractAntiForgeryToken(response); var formPostBodyData = new Dictionary{ {"__RequestVerificationToken", antiForgeryToken}, // Add token {"Awesomesauce.Foo", "Bar"}, {"Awesomesauce.AnotherKey", "Baz"}, {"Any_Other_Form_Key", "Any_Other_Value"} }; // Copy cookies from response var requestMessage = PostRequestHelper.CreateWithCookiesFromResponse("/awesomesauce", formPostBodyData, response); var response = await client.SendAsync(requestMessage); // Assert } }
Conclusion
After we have set up integration testing we want to get to do some basic interactions with our application. Using an AntiRequestForgeryHelper we can extract a token. Using the PostRequestHelper we can construct a new Request to easily send over a request. Combined they can make any scenario work with CSRF protection.
In case you need to pass over cookies information you can use the CookiesHelper.