|
2 | 2 |
|
3 | 3 | This section will cover HTTP related testing and user identity authentication. |
4 | 4 |
|
5 | | -## HTTP requests |
| 5 | +## HTTP request |
6 | 6 |
|
| 7 | +Sometimes we need to process the HTTP request in the controller action. Take a look at the HTTP Post overload of the **"AddressAndPayment"** action in the **"CheckoutController"**: |
| 8 | + |
| 9 | +```c# |
| 10 | +// action code skipped for brevity |
| 11 | +
|
| 12 | +var formCollection = await HttpContext.Request.ReadFormAsync(); |
| 13 | + |
| 14 | +try |
| 15 | +{ |
| 16 | + if (string.Equals(formCollection["PromoCode"].FirstOrDefault(), PromoCode, |
| 17 | + StringComparison.OrdinalIgnoreCase) == false) |
| 18 | + { |
| 19 | + return View(order); |
| 20 | + } |
| 21 | + |
| 22 | +// action code skipped for brevity |
| 23 | +``` |
| 24 | + |
| 25 | +The action reads the form and checks for an input named **"PromoCode"**. If it does not equals **"FREE"**, the action return its view with the same order provided from the form. Let's test this logic! |
| 26 | + |
| 27 | +Go to the **"project.json"** file and add **"MyTested.AspNetCore.Mvc.Http"** as a dependency: |
| 28 | + |
| 29 | +```json |
| 30 | +"dependencies": { |
| 31 | + "dotnet-test-xunit": "2.2.0-*", |
| 32 | + "xunit": "2.2.0-*", |
| 33 | + "Moq": "4.6.38-*", |
| 34 | + "MyTested.AspNetCore.Mvc.Controllers": "1.0.0", |
| 35 | + "MyTested.AspNetCore.Mvc.DependencyInjection": "1.0.0", |
| 36 | + "MyTested.AspNetCore.Mvc.EntityFrameworkCore": "1.0.0", |
| 37 | + "MyTested.AspNetCore.Mvc.Http": "1.0.0", // <--- |
| 38 | + "MyTested.AspNetCore.Mvc.ModelState": "1.0.0", |
| 39 | + "MyTested.AspNetCore.Mvc.Models": "1.0.0", |
| 40 | + "MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0", |
| 41 | + "MusicStore": "*" |
| 42 | +}, |
| 43 | +``` |
| 44 | + |
| 45 | +This package will provide you with additional methods - two of them are **"WithHttpContext"** and **"WithHttpRequest"**. We will use the second one - it provides a fast way to set up every single part of the HTTP request. |
| 46 | + |
| 47 | +Go to the **"CheckoutControllerTest"** and add the following test: |
| 48 | + |
| 49 | +```c# |
| 50 | +[Fact] |
| 51 | +public void AddressAndPaymentShouldRerurnViewWithInvalidPostedPromoCode() |
| 52 | + => MyController<CheckoutController> |
| 53 | + .Instance() |
| 54 | + .WithHttpRequest(request => request // <--- |
| 55 | + .WithFormField("PromoCode", "Invalid")) |
| 56 | + .Calling(c => c.AddressAndPayment( |
| 57 | + From.Services<MusicStoreContext>(), |
| 58 | + With.Default<Order>(), |
| 59 | + CancellationToken.None)) |
| 60 | + .ShouldHave() |
| 61 | + .ValidModelState() |
| 62 | + .AndAlso() |
| 63 | + .ShouldReturn() |
| 64 | + .View() |
| 65 | + .WithModel(With.Default<Order>()); |
| 66 | +``` |
| 67 | + |
| 68 | +We have successfully tested that with an invalid promo code in the request form, our action should return the same view with the proper model. The **"WithHttpRequest"** method allows you to add form fields, files, headers, body, cookies and more. We will see more of it when we cover route testing. |
| 69 | + |
| 70 | +## Authentication |
| 71 | + |
| 72 | +Now let's take a look at the **"Complete"** action in the same controller: |
| 73 | + |
| 74 | +```c# |
| 75 | +// action code skipped for brevity |
| 76 | +
|
| 77 | +var userName = HttpContext.User.Identity.Name; |
| 78 | + |
| 79 | +bool isValid = await dbContext.Orders.AnyAsync( |
| 80 | + o => o.OrderId == id && |
| 81 | + o.Username == userName); |
| 82 | + |
| 83 | +if (isValid) |
| 84 | +{ |
| 85 | + return View(id); |
| 86 | +} |
| 87 | +else |
| 88 | +{ |
| 89 | + return View("Error"); |
| 90 | +} |
| 91 | + |
| 92 | +// action code skipped for brevity |
| 93 | +``` |
| 94 | + |
| 95 | +By default tests do not have an authenticated user identity. Write this test in the **"CheckoutControllerTest"**, run it and see for yourself: |
| 96 | + |
| 97 | +```c# |
| 98 | +[Fact] |
| 99 | +public void CompleteShouldReturnViewWithCorrectIdWithFoundOrderForTheUser() |
| 100 | + => MyController<CheckoutController> |
| 101 | + .Instance() |
| 102 | + .WithDbContext(db => db |
| 103 | + .WithEntities(entities => entities.Add(new Order |
| 104 | + { |
| 105 | + OrderId = 1, |
| 106 | + Username = "TestUser" |
| 107 | + }))) |
| 108 | + .Calling(c => c.Complete(From.Services<MusicStoreContext>(), 1)) |
| 109 | + .ShouldReturn() |
| 110 | + .View() |
| 111 | + .WithModel(1); |
| 112 | +``` |
| 113 | + |
| 114 | +It fails. Obviously, we need an authenticated user to test this action. We can attach it to the **"HttpContext"** but let's make it easier. Head over to the **"project.json"** file again and add **"MyTested.AspNetCore.Mvc.Authentication"**: |
| 115 | + |
| 116 | +"dependencies": { |
| 117 | + "dotnet-test-xunit": "2.2.0-*", |
| 118 | + "xunit": "2.2.0-*", |
| 119 | + "Moq": "4.6.38-*", |
| 120 | + "MyTested.AspNetCore.Mvc.Authentication": "1.0.0", // <--- |
| 121 | + "MyTested.AspNetCore.Mvc.Controllers": "1.0.0", |
| 122 | + "MyTested.AspNetCore.Mvc.DependencyInjection": "1.0.0", |
| 123 | + "MyTested.AspNetCore.Mvc.EntityFrameworkCore": "1.0.0", |
| 124 | + "MyTested.AspNetCore.Mvc.Http": "1.0.0", |
| 125 | + "MyTested.AspNetCore.Mvc.ModelState": "1.0.0", |
| 126 | + "MyTested.AspNetCore.Mvc.Models": "1.0.0", |
| 127 | + "MyTested.AspNetCore.Mvc.ViewActionResults": "1.0.0", |
| 128 | + "MusicStore": "*" |
| 129 | +}, |
| 130 | + |
| 131 | +**"WithAuthenticatedUser"** method will be added to the fluent API. You can use it to set identifier, username, roles, claims and identities. But for now call it empty like this: |
| 132 | + |
| 133 | +```c# |
| 134 | +[Fact] |
| 135 | +public void CompleteShouldReturnViewWithCorrectIdWithFoundOrderForTheUser() |
| 136 | + => MyController<CheckoutController> |
| 137 | + .Instance() |
| 138 | + .WithAuthenticatedUser() // <--- |
| 139 | + .WithDbContext(db => db |
| 140 | + .WithEntities(entities => entities.Add(new Order |
| 141 | + { |
| 142 | + OrderId = 1, |
| 143 | + Username = "TestUser" |
| 144 | + }))) |
| 145 | + .Calling(c => c.Complete(From.Services<MusicStoreContext>(), 1)) |
| 146 | + .ShouldReturn() |
| 147 | + .View() |
| 148 | + .WithModel(1); |
| 149 | +``` |
| 150 | + |
| 151 | +You will receive a passing test because the default authenticated user has **"TestId"** identifier and **"TestUser"** username. Change the order **"Username"** property to **"MyTestUser"** and you will need to provide the username of the identity in order to make the test pass again: |
| 152 | + |
| 153 | +```c# |
| 154 | +[Fact] |
| 155 | +public void CompleteShouldReturnViewWithCorrectIdWithFoundOrderForTheUser() |
| 156 | + => MyController<CheckoutController> |
| 157 | + .Instance() |
| 158 | + .WithAuthenticatedUser(user => user // <--- |
| 159 | + .WithUsername("MyTestUser")) |
| 160 | + .WithDbContext(db => db |
| 161 | + .WithEntities(entities => entities.Add(new Order |
| 162 | + { |
| 163 | + OrderId = 1, |
| 164 | + Username = "MyTestUser" |
| 165 | + }))) |
| 166 | + .Calling(c => c.Complete(From.Services<MusicStoreContext>(), 1)) |
| 167 | + .ShouldReturn() |
| 168 | + .View() |
| 169 | + .WithModel(1); |
| 170 | +``` |
| 171 | + |
| 172 | +Of course, we need to also test the result when the order is not for the currently authenticated user. In this case we need to return the **"Error"** view: |
| 173 | + |
| 174 | +```c# |
| 175 | +[Fact] |
| 176 | +public void CompleteShouldReturnErrorViewWithInvalidOrderForTheUser() |
| 177 | + => MyController<CheckoutController> |
| 178 | + .Instance() |
| 179 | + .WithAuthenticatedUser(user => user |
| 180 | + .WithUsername("InvalidUser")) |
| 181 | + .WithDbContext(db => db |
| 182 | + .WithEntities(entities => entities.Add(new Order |
| 183 | + { |
| 184 | + OrderId = 1, |
| 185 | + Username = "MyTestUser" |
| 186 | + }))) |
| 187 | + .Calling(c => c.Complete(From.Services<MusicStoreContext>(), 1)) |
| 188 | + .ShouldReturn() |
| 189 | + .View("Error"); |
| 190 | +``` |
| 191 | + |
| 192 | +## HTTP Response |
| 193 | + |
| 194 | +Sometimes we may manipulate the HTTP response directly in the controller action - for example to add a custom header. The Music Store web application does not have such logic but we can take any action and validate whether it returns 200 (OK) status code just for the sake of seeing the syntax. |
| 195 | + |
| 196 | +Create a **"HomeControllerTest"** class and add the following test: |
| 197 | + |
| 198 | +```c# |
| 199 | +[Fact] |
| 200 | +public void AccessDeniedShouldReturnOkStatusCodeAndProperView() |
| 201 | + => MyController<HomeController> |
| 202 | + .Instance() |
| 203 | + .Calling(c => c.AccessDenied()) |
| 204 | + .ShouldHave() |
| 205 | + .HttpResponse(response => response // <--- |
| 206 | + .WithStatusCode(HttpStatusCode.OK)) |
| 207 | + .AndAlso() |
| 208 | + .ShouldReturn() |
| 209 | + .View("~/Views/Shared/AccessDenied.cshtml"); |
| 210 | +``` |
| 211 | + |
| 212 | +The **"HttpResponse"** method allows assertions of every part of the HTTP response - body, headers, cookies, etc. For example if you add this line: |
| 213 | + |
| 214 | +```c# |
| 215 | +.ContainingHeader("InvalidHeader") |
| 216 | +``` |
| 217 | + |
| 218 | +You will receive a nice little error message (as always): |
| 219 | + |
| 220 | +``` |
| 221 | +When calling AccessDenied action in HomeController expected HTTP response headers to contain header with 'InvalidHeader' name, but such was not found. |
| 222 | +``` |
| 223 | + |
| 224 | +Cool! :) |
| 225 | + |
| 226 | +## Section summary |
| 227 | + |
| 228 | +Well, these were easier than the last section's test services. While the request testing is more suitable on other components, authentication plays big role in the actions' logic. |
| 229 | + |
| 230 | +If you followed the tutorial strictly, you should have reached the free trial version limitations of My Tested ASP.NET Core MVC. Let's take a break from the code and learn more about the [Licensing](/tutorial/licensing.html) of the testing framework. |
0 commit comments