Skip to content

Commit 4a67f4a

Browse files
committed
Added HTTP & Authentication section to the tutorial (#132)
1 parent bf8df69 commit 4a67f4a

4 files changed

Lines changed: 509 additions & 2 deletions

File tree

docs/_docfx/tutorial/httpauthentication.md

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,229 @@
22

33
This section will cover HTTP related testing and user identity authentication.
44

5-
## HTTP requests
5+
## HTTP request
66

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.

docs/manifest.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)