I have a .NET C# Web API application. I have a single url controller endpoint which receives a POST message. When I run the app and I use an external tool to send the POST message it works perfectly fine.
However, when I trigger the controller from my unit test I get a null ref. exception because for some reason HttpContext.Current is null
This is my current controller (which works in a real scenario):
[HttpPost]
public async Task<IHttpActionResult> Post()
{
await Request.Content.ReadAsStringAsync();
if (Request.Content.IsFormData())
{
var stuff = HttpContext.Current.Request["stuff"];
}
return Ok();
}
}
This is my unit test file:
[TestFixture]
public class AnnotationsControllerTest : BaseIntegrationTest
{
private const string Uri = "http://localhost:2622/api/annotations";
[Test]
public async void TestHistoriesPost()
{
var form = new List<KeyValuePair<string, string>>();
form.Add(new KeyValuePair<string, string>("stuff", "123456"));
using (var request = new HttpRequestMessage(HttpMethod.Post, Uri))
using (var config = new HttpConfiguration())
{
var route = config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}");
using (var content = new FormUrlEncodedContent(form))
{
request.Content = content;
var mockDataService = GetDataServices();
var controller = new AnnotationsController(mockDataService.Object, ApiTestConfiguration());
SetupController(route, controller, config, request);
var actionResult = await controller.Post();
var httpResponseMessage = await actionResult.ExecuteAsync(CancellationToken.None);
Assert.AreEqual(HttpStatusCode.OK, httpResponseMessage.StatusCode);
}
}
}
private static void SetupController(
IHttpRoute route,
ApiController controller,
HttpConfiguration configuration,
HttpRequestMessage request)
{
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary { { "controller", "Annotations" } });
controller.ControllerContext = new HttpControllerContext(configuration, routeData, request);
configuration.Services.Replace(typeof(IExceptionHandler), new UnhandledExceptionHandler());
controller.Request = request;
controller.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = configuration;
}
private Mock<IDataServices> GetDataServices()
{
return new Mock<IDataServices>();
}
}
Related
I have a web api end point that i want to unit test. I have a custom SwaggerUploadFile attribute that allows a file upload button on the swagger page. But for unit testing I cant figure out how to pass in a file.
For unit testing I am using: Xunit, Moq and Fluent Assertions
Below is my controller with the endpoint:
public class MyAppController : ApiController
{
private readonly IMyApp _myApp;
public MyAppController(IMyApp myApp)
{
if (myApp == null) throw new ArgumentNullException(nameof(myApp));
_myApp = myApp;
}
[HttpPost]
[ResponseType(typeof(string))]
[Route("api/myApp/UploadFile")]
[SwaggerUploadFile("myFile", "Upload a .zip format file", Required = true, Type = "file")]
public async Task<IHttpActionResult> UploadFile()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = await Request.Content.ReadAsMultipartAsync();
var bytes = await provider.Contents.First().ReadAsByteArrayAsync();
try
{
var retVal = _myApp.CheckAndSaveByteStreamAsync(bytes).Result;
if(retVal)
{
return
ResponseMessage(
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "File has been saved"
}), Encoding.UTF8, "application/json")
});
}
return ResponseMessage(
new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "The file could not be saved"
}), Encoding.UTF8, "application/json")
});
}
catch (Exception e)
{
//log error
return BadRequest("Oops...something went wrong");
}
}
}
Unit test I have so far:
[Fact]
[Trait("Category", "MyAppController")]
public void UploadFileTestWorks()
{
//Arrange
_myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
var expected = JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "The file has been saved"
});
var _sut = new MyAppController(_myApp.Object);
//Act
var retVal = _sut.UploadFile();
var content = (ResponseMessageResult)retVal.Result;
var contentResult = content.Response.Content.ReadAsStringAsync().Result;
//Assert
contentResult.Should().Be(expected);
}
The above fails as when it hits this line if(!Request.Content.IsMimeMultipartContent()) we get a NullReferenceException > "{"Object reference not set to an instance of an object."}"
Best Answer Implemented:
Created an interface:
public interface IApiRequestProvider
{
Task<MultipartMemoryStreamProvider> ReadAsMultiPartAsync();
bool IsMimeMultiPartContent();
}
Then an implementation:
public class ApiRequestProvider : ApiController, IApiRequestProvider
{
public Task<MultipartMemoryStreamProvider> ReadAsMultiPartAsync()
{
return Request.Content.ReadAsMultipartAsync();
}
public bool IsMimeMultiPartContent()
{
return Request.Content.IsMimeMultipartContent();
}
}
Now my controller uses constructor injection to get the RequestProvider:
private readonly IMyApp _myApp;
private readonly IApiRequestProvider _apiRequestProvider;
public MyAppController(IMyApp myApp, IApiRequestProvider apiRequestProvider)
{
if (myApp == null) throw new ArgumentNullException(nameof(myApp));
_myApp = myApp;
if (apiRequestProvider== null) throw new ArgumentNullException(nameof(apiRequestProvider));
_apiRequestProvider= apiRequestProvider;
}
New implementation on method:
[HttpPost]
[ResponseType(typeof(string))]
[Route("api/myApp/UploadFile")]
[SwaggerUploadFile("myFile", "Upload a .zip format file", Required = true, Type = "file")]
public async Task<IHttpActionResult> UploadFile()
{
if (!_apiRequestProvider.IsMimeMultiPartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = await _apiRequestProvider.ReadAsMultiPartAsync();
var bytes = await provider.Contents.First().ReadAsByteArrayAsync();
try
{
var retVal = _myApp.CheckAndSaveByteStreamAsync(bytes).Result;
if(retVal)
{
return
ResponseMessage(
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "File has been saved"
}), Encoding.UTF8, "application/json")
});
}
return ResponseMessage(
new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "The file could not be saved"
}), Encoding.UTF8, "application/json")
});
}
catch (Exception e)
{
//log error
return BadRequest("Oops...something went wrong");
}
}
}
And my unit test that mocks the ApiController Request:
[Fact]
[Trait("Category", "MyAppController")]
public void UploadFileTestWorks()
{
//Arrange
_apiRequestProvider = new Mock<IApiRequestProvider>();
_myApp = new Mock<IMyApp>();
MultipartMemoryStreamProvider fakeStream = new MultipartMemoryStreamProvider();
fakeStream.Contents.Add(CreateFakeMultiPartFormData());
_apiRequestProvider.Setup(x => x.IsMimeMultiPartContent()).Returns(true);
_apiRequestProvider.Setup(x => x.ReadAsMultiPartAsync()).ReturnsAsync(()=>fakeStream);
_myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
var expected = JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "The file has been saved"
});
var _sut = new MyAppController(_myApp.Object, _apiRequestProvider.Object);
//Act
var retVal = _sut.UploadFile();
var content = (ResponseMessageResult)retVal.Result;
var contentResult = content.Response.Content.ReadAsStringAsync().Result;
//Assert
contentResult.Should().Be(expected);
}
Thanks to #Badulake for the idea
You should do a better separation in the logic of the method.
Refactor your method so it does not depends on any class related to your web framework , in this case the Request class. Your upload code does not need to know anything about it.
As a hint:
var provider = await Request.Content.ReadAsMultipartAsync();
could be transformed to:
var provider = IProviderExtracter.Extract();
public interface IProviderExtracter
{
Task<provider> Extract();
}
public class RequestProviderExtracter:IProviderExtracter
{
public Task<provider> Extract()
{
return Request.Content.ReadAsMultipartAsync();
}
}
In your tests you can easely mock IProviderExtracter and focus in which work is doing each part of your code.
The idea is you get the most decoupled code so your worries are focused only in mocking the classes you have developed, no the ones the framework forces you to use.
The below was how I initially solved it but after Badulake's answer i implemented that where i abstracted the api request to an interface/class and Mocked it out with Moq. I edited my question and put the best implementation there, but i left this answer here for people who dont want to go to the trouble of mocking it
I used part of this guide but I made a simpler solution:
New unit test:
[Fact]
[Trait("Category", "MyAppController")]
public void UploadFileTestWorks()
{
//Arrange
var multiPartContent = CreateFakeMultiPartFormData();
_myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
var expected = JsonConvert.SerializeObject(
new WebApiResponse
{
Message = "The file has been saved"
});
_sut = new MyAppController(_myApp.Object);
//Sets a controller request message content to
_sut.Request = new HttpRequestMessage()
{
Method = HttpMethod.Post,
Content = multiPartContent
};
//Act
var retVal = _sut.UploadFile();
var content = (ResponseMessageResult)retVal.Result;
var contentResult = content.Response.Content.ReadAsStringAsync().Result;
//Assert
contentResult.Should().Be(expected);
}
Private support method:
private static MultipartFormDataContent CreateFakeMultiPartFormData()
{
byte[] data = { 1, 2, 3, 4, 5 };
ByteArrayContent byteContent = new ByteArrayContent(data);
StringContent stringContent = new StringContent(
"blah blah",
System.Text.Encoding.UTF8);
MultipartFormDataContent multipartContent = new MultipartFormDataContent { byteContent, stringContent };
return multipartContent;
}
I am starting WebApi tutorial but I just faced a problem that parameter in action is a null.
Below is the tutorial code.
WebApi
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var raw = Request.Content.ReadAsStringAsync();
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, Result.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
WebApi Client
static string Register(string email, string password)
{
var Result = new RegisterBindingModel()
{
Email = email,
Password = password,
ConfirmPassword = password
};
using (var client = new HttpClient())
{
var response = client.PostAsJsonAsync(
"http://localhost:7399/api/Account/Register",
Result).Result;
return response.StatusCode.ToString();
}
}
Register action receives http request but model is always null. So the raw variable shows like this
{"Email":"test#gmail.com","Password":"Test#123","ConfirmPassword":"Test#123"}
But when I tried sending http request using Postman it worked. As request body was read for model binding. The raw variable was empty. I don't know what's wrong with my client. I followed exactly tutorial code. Should I specify content-type?
make variable name same i.e. in Register method change var Result to var model and have a try.
it should be frombody , i.e. get parameter value from post body
[HttpPost]
public async Task<IHttpActionResult> Register([FromBody]RegisterBindingModel model)
Add HttpPost And Use below Methods
[AllowAnonymous]
[HttpPost]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
//your code
}
Use this Methods:-
public static async Task<HttpResponseMessage> PostAsJsonAsync(this HttpClient client,
string requestUri, object requestObject, Dictionary<string, string> requestHeaders = null,
int? timeoutMilliSeconds = null)
{
var jsonContent = JsonConvert.SerializeObject(requestObject);
var requestContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
return await SendAsync(client, requestUri, HttpMethod.Post,
requestContent, requestHeaders, timeoutMilliSeconds);
}
public static async Task<HttpResponseMessage> SendAsync(
this HttpClient client,
string requestUri, HttpMethod httpMethod, HttpContent requestContent = null,
Dictionary<string, string> requestHeaders = null, int? timeoutMilliSeconds = null)
{
var httpRequestMessage = new HttpRequestMessage
{
RequestUri = new Uri(requestUri),
Method = httpMethod,
Content = requestContent
};
if (requestHeaders != null)
{
foreach (var requestHeader in requestHeaders)
{
httpRequestMessage.Headers.Add(requestHeader.Key, requestHeader.Value);
}
}
if (timeoutMilliSeconds.HasValue)
{
var cts = new CancellationTokenSource();
cts.CancelAfter(new TimeSpan(0, 0, 0, 0, timeoutMilliSeconds.Value));
return await client.SendAsync(httpRequestMessage, cts.Token);
}
else
{
return await client.SendAsync(httpRequestMessage);
}
}
I need to use HttpClientFactory for connection to external api.
The code in Startup.cs looks this
public void SetUpHttpClients(IServiceCollection services)
{
var loginEndpoint = Path.Combine(baseApi, "api/authentication);
var fileExists = File.Exists(certificatePath);
if (!fileExists)
throw new ArgumentException(certificatePath);
var certificate = new X509Certificate2(certificatePath, certPwd);
services.AddHttpClient("TestClient", client =>
{
client.BaseAddress = new Uri(baseApi);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(accept));
client.DefaultRequestHeaders.Add("ApiKey", apiKey);
var body = new { Username = username, Password = password };
var jsonBody = JsonConvert.SerializeObject(body);
var content = new StringContent(jsonBody, Encoding.UTF8, contentType);
var loginResponse = client.PostAsync(loginEndpoint, content).Result;
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
handler.ClientCertificates.Add(certificate);
return handler;
});
I've added this code in MessageController with HttpClientFactory,
public class MessagesController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public ValuesController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet]
public IActionResult GetMessages()
{
var client = _httpClientFactory.CreateClient("TestClient");
var result = client.GetStringAsync("/api/messages");
return Ok(result);
}
}
but when I try to connection and get messages from HttpClientFactory with I get this error:
Failed to load resource: net::ERR_CONNECTION_RESET, 404 - BadRequest
You trying to make a request before completing the factory configuration That Post will fail because you are trying to use the client while still configuring it.
The assumption here is that SetUpHttpClients is being called within ConfigureServices.
public void ConfigureServices(IServiceCollection services) {
//...
SetUpHttpClients(services);
//...
services.AddMvc();
}
public void SetUpHttpClients(IServiceCollection services) {
var basePath = Directory.GetCurrentDirectory();
var certificatePath = Path.Combine(basePath, certPath);
var fileExists = File.Exists(certificatePath);
if (!fileExists)
throw new ArgumentException(certificatePath);
var certificate = new X509Certificate2(certificatePath, certPwd);
//Adding a named client
services.AddHttpClient("TestClient", client => {
client.BaseAddress = new Uri(baseApi);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(accept));
client.DefaultRequestHeaders.Add("ApiKey", apiKey);
})
.ConfigurePrimaryHttpMessageHandler(() => {
var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler {
CookieContainer = cookieContainer
};
handler.ClientCertificates.Add(certificate);
return handler;
});
}
With the configuration done, the factory is available for injection into a controller or service class as needed.
The following example shows using the factory as a dependency to a controller.
[Route("api/[controller]")]
public class ValuesController : Controller {
private readonly IHttpClientFactory factory;
public MyController(IHttpClientFactory factory) {
this.factory = factory;
}
[HttpGet]
public async Task<IActionResult> Get() {
var client = factory.CreateClient("TestClient");
var result = client.GetStringAsync("api/messages");
return Ok(result);
}
}
In the above the factory is used to create an instance of the named client. It will have all the configuration that was set during start up, which would include the client handler with certificate.
To be able to use the client during start up (which I would advise against), you would first need to build the service collection into a service provider.
var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetService<IHttpClientFactory>();
var client = factory.CreateClient("TestClient");
var body = new { Username = username, Password = password };
var jsonBody = JsonConvert.SerializeObject(body);
var content = new StringContent(jsonBody, Encoding.UTF8, contentType);
var loginResponse = client.PostAsync("api/authentication", content).Result;
//...do something with the response
This can however have unforeseen results as the service collection would not have been completely populated at this stage in the start up process.
I've resolve this issue. SetUpHttpClient method look this:
public void SetUpHttpClients(IServiceCollection services)
{
var basePath = Directory.GetCurrentDirectory();
var certificatePath = Path.Combine(basePath, CertPath);
var fileExists = File.Exists(certificatePath);
if (!fileExists)
throw new ArgumentException(certificatePath);
var certificate = new X509Certificate2(certificatePath, CertPwd);
services.AddHttpClient("TestClient", client =>
{
client.BaseAddress = new Uri(BaseApi);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Accept));
client.DefaultRequestHeaders.Add("ApiKey", ApiKey);
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
return handler;
});
}
The rest of code for login and messages I've added to service and call from controller.
MessageClient.cs
public MessageClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_client = _httpClientFactory.CreateClient("TestClient");
var body = new { UserName = Username, UserPassword = Password };
var jsonBody = JsonConvert.SerializeObject(body);
var content = new StringContent(jsonBody, Encoding.UTF8, ContentType);
var loginEndpoint = Path.Combine(BaseApi, "api/authentication");
_responseMessage = _client.PostAsync(loginEndpoint, content).Result;
}
public async Task<string> ReadMessages()
{
try
{
string messages = "";
if (_responseMessage.IsSuccessStatusCode)
{
var messagesEndpoint = Path.Combine(BaseApi, "api/messages/");
var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cookieContainer };
var cookiesToSet = GetCookiesToSet(_responseMessage.Headers, BaseApi);
foreach (var cookie in cookiesToSet)
{
handler.CookieContainer.Add(cookie);
}
var messageResponse = await _client.GetAsync(messagesEndpoint);
messages = await messageResponse.Content.ReadAsStringAsync();
}
return messages;
}
catch (Exception e)
{
throw e.InnerException;
}
}
MessageContoller.cs
[HttpGet]
public async Task<ActionResult> GetMessages()
{
var result = await _messageClient.ReadMessages();
return Ok(result);
}
Thanks #Nkosi.
I have two projects, one is client other is API provider. When I send request to api provider 'request.Headers.GetCookies()' have values on the client part but in
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var test1 = context.Request.Headers.ToList().FirstOrDefault(h => h.Key == "Cookie");
test1 is null and in
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var test2 = actionContext.Request.Headers.GetCookies();
test2 is null, this is how I send request:
[HttpPost]
public ActionResult PostToApiProvider(RequestViewModel requestModel)
{
var request = new HttpResponseMessage();
// Here I add cookie header in request for authentification
if (HttpContext.Request.Cookies.Count != 0)
{
var cookieList = new List<string>();
for (var i = 0; i < Request.Cookies.Count; i++)
{
cookieList.Add(Request.Cookies[i].Value);
}
request.Headers.Add("Cookie", cookieList);
}
var response = client.SendAsync(request).Result;
var testRequestCookies = request.Headers.GetCookies();
return View("SomeView", model)
request and testRequestCookies (before and after call to api provider) will have cookie values , please see the screenshoot :
CookieObject
If I can help with more code, please ask. Thanks :)
Here's how to get a cookie value
private async Task<string> GetCookieValue(string url, string cookieName)
{
var cookieContainer = new CookieContainer();
var uri = new Uri(url);
using (var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
})
{
using (var httpClient = new HttpClient(httpClientHandler))
{
await httpClient.GetAsync(uri);
var cookie = cookieContainer.GetCookies(uri).Cast<Cookie>().FirstOrDefault(x => x.Name == cookieName);
return cookie?.Value;
}
}
}
I'm trying to come up with a way to post to a Web API controller with one object but have a different, processed, object return. None of the methods I've been able to find have solved the issue.
Here's my method in my MVC project that posts to my Web API project
public dynamic PostStuff<X>(string action, X request)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var task = client.PostAsJsonAsync(new Uri("host/api/somecontroller/post, request);
task.Wait();
var response = task.Result;
return response;
}
}
This is my Web API controller code
public HttpResponseMessage Post([FromBody] FooObject foo)
{
var res = Request.CreateResponse(HttpStatusCode.OK,
new ValidationResponse<FooObject>
{
ID = new Random().Next(1000, 1000000),
Content =
new List<IContent>()
{
{
new Content()
{
Name = "TheContent",
Type = "SomeType",
Value = "This is some content for the page : " + foo.Bar
}
}
},
Product = new ProductFoo(),
Validated = true
});
return res;
}
}
When I put a break in my WebAPI controller code, the res variable is correctly created. Once the processing goes back to the PostStuff method, all I get is a StreamResponse with no trace of the ValidationResponse object created in the Web API controller. There are no errors but I can't use anything in the response beyond that the post succeeded. How can I extract the ValidationResponse from my posting method?
I have built myself this generic function as a helper for POSTing to Web API
Usage
var result = PostAsync<MyDataType, MyResultType>("http://...", MyData)
Function
public async Task<U> PostAsync<T, U>(string url, T model)
{
using (HttpClient httpClient = new HttpClient(httpHandler))
{
var result = await httpClient.PostAsJsonAsync<T>(url, model);
if (result.IsSuccessStatusCode == false)
{
string message = await result.Content.ReadAsStringAsync();
throw new Exception(message);
}
else
{
return await result.Content.ReadAsAsync<U>();
}
}
}
And HttpClientHandler configured for Windows Auth
protected HttpClientHandler httpHandler = new HttpClientHandler() { PreAuthenticate = true, UseDefaultCredentials = true };