I'm trying to setup a test project for my asp.net core web API. There are a couple of specialties I need to handle:
Use a different DB for the tests
Getting the UserManger to seed some users
According to the Documentation I should derive form WebApplicationFactory and override the ConfigureWebHost method. So I did:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
public BeepDbContext Context;
public UserManager<User> UserMgr;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
ServiceDescriptor descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<BeepDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<BeepDbContext>(o =>
o.UseSqlServer("Server=.\\SQLEXPRESS;Database=BeepTest;Trusted_Connection=true;"));
ServiceProvider provider = services.BuildServiceProvider();
using (IServiceScope scope = provider.CreateScope())
{
Context = scope.ServiceProvider.GetRequiredService<BeepDbContext>();
Context.Database.Migrate();
UserMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>(); // <--- fails here
}
});
}
}
and this is my test class:
public class UserControllerTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly ITestOutputHelper _output;
private readonly CustomWebApplicationFactory _factory;
public UserControllerTests(ITestOutputHelper output, CustomWebApplicationFactory factory)
{
_output = output;
_factory = factory;
}
[Fact]
public async Task Login()
{
HttpClient client = _factory.CreateClient();
HttpResponseMessage result = await client.PostAsJsonAsync("/api/auth/login", new UserForLoginDto()
{
Username = "user",
Password = "P#ssw0rd"
});
_output.WriteLine(result.StatusCode.ToString());
_output.WriteLine(result.Content.ReadAsStringAsync().Result);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
}
This fails on the line where I try to get the UserManager saying that there is no service registered for this type. Also I was unable to make it use the config file from the test project instead of the main project. Hence I hard coded the connection string. After a bit of Debugging I've found out that the ConfigureServices method from the startup Class is executed after the one in my override. So I think this explains why I can't get the User manager. What I don't understand is, what am I doing wrong or how is this done properly?
As far as I can tell I'm doing pretty much the same as in the Documentation or the sample app they provide in the Documentation.
Related
I have an authorization handler that needs to pull data from the database to complete the authorization logic. The idea is that users are only allowed to certain areas after posting a given number of blog posts.
Code as follows:
namespace MyProject.Authorisation
{
public class MinimumPostRequirement : IAuthorizationRequirement
{
public MinimumPostRequirement (int postCount)
{
PostCount = postCount;
}
public int PostCount { get; }
}
public class MinimumPostRequirement Handler : AuthorizationHandler<MinimumPostRequirement >
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext authContext, ApprovedUserRequirement requirement)
{
using (MyDbContext _context = new MyDbContext())
{
int? postCount = _context.Posts.Where(post => post.UserName == authContext.User.Identity.Name).Count();
if(postCount == null)
{
return Task.CompletedTask;
}
if(postCount >= requirement.PostCount)
{
authContext.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
}
Here is how I declare it in Program.cs:
//DB Connection
var connectionString = builder.Configuration.GetConnectionString("MyConnection");
builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(MyConnection));
//authorisation
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireMinimumPosts", policy => policy.Requirements.Add(new MinimumPostRequirement(3)));
});
builder.Services.AddSingleton<IAuthorizationHandler, MinimumPostRequirementHandler>();
I know there is a dependency injection issue when adding a singleton, so I have also tried using in Program.cs:
builder.Services.AddScoped<IAuthorizationHandler, MinimumPostRequirementHandler>();
And
builder.Services.AddTransient<IAuthorizationHandler, MinimumPostRequirementHandler>();
All result in the following error:
InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.
The database works for all other site operations. The problem only arises when I add [Authorize(Policy = "RequireMinimumPosts")] to the methods I want to restrict.
How would you write this code so that it works? How does dependency injection work in this context? Is there anything I am missing?
Inject the DbContext into the constructor of your MinimumPostRequirementHandler so it will be resolved by the DI container.
public class MinimumPostRequirementHandler
: AuthorizationHandler<MinimumPostRequirement>
{
private readonly MyDbContext _dbContext;
public MinimumPostRequirementHandler( MyDbContext dbContext )
{
_dbContext = dbContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext authContext, ApprovedUserRequirement requirement)
{
// use _dbContext here
}
}
Register the handler as scoped.
I have an isolated Azure Function that makes couple of HTTP POST calls. I am trying to write an integration test for them. But the test setup fails with a gRPC error.
Here is the Program.cs that configures the HttpClient with services.AddHttpClient();
public class Program
{
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddHttpClient();
}).Build();
host.Run();
}
}
The sample function looks like this:
public class AzFunctionEndpoint
{
public AzFunctionEndpoint(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger<ResolveEndpoint>();
this.httpClientFactory = httpClientFactory;
this.configuration = configuration;
}
[Function("azfunction")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "azs/azfunction")] HttpRequestData req)
{
// Couple of other HTTP calls using httpClientFactory
// return
var res = req.CreateResponse(HttpStatusCode.OK);
return res;
}
private readonly IConfiguration configuration;
private readonly IHttpClientFactory httpClientFactory;
private readonly ILogger logger;
}
The Function runs correctly on local machine and when deployed to Azure.
Now I try and create an integration test with
public class AzFunctionEndpointIntegrationTests
{
public AzFunctionEndpointIntegrationTests()
{
factory = new WebApplicationFactory<Program>();
var clientFactory = factory.Services.GetService<IHttpClientFactory>();
// THIS LINE CAUSES gRPC host error
client = clientFactory.CreateClient();
}
[Fact]
public async Task AzFunction_Should_BeOK()
{
// POST to the azure function
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(HttpMethods.Post), "api/azs/azfunction");
var response = await client.SendAsync(request);
response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
}
private HttpClient client;
private WebApplicationFactory<Program> factory;
}
The test code that tries to create HttpClient to invoke my function causes this exception
client = clientFactory.CreateClient();
System.InvalidOperationException : The gRPC channel URI 'http://:51828' could not be parsed.
I don't quite understand what is this error ! Any help on writing integration tests for Azure Functions is appreciated.
System.InvalidOperationException : The gRPC channel URI
'http://:51828' could not be parsed.
I don't quite understand what is this error ! Any help on writing
integration tests for Azure Functions is appreciated.
Below are the few workaround may help to fix the above issue.
Based on this GitHub issue the root cause we have observed that, May be due to the usage of incorrect run/debug configuration the IDE you are using.
Please make sure that you are using the same as suggested on the given gitHub link to run the application.
To write an Integration test you can refer this Example for your function app.
Example of sample code:-
test.cs
[TestClass]
public class DefaultHttpTriggerTests
{
private HttpClient _http;
[TestInitialize]
public void Initialize()
{
this._http = new HttpClient();
}
[TestCleanup]
public void Cleanup()
{
this._http.Dispose();
}
[TestMethod]
public async Task Given_OpenApiUrl_When_Endpoint_Invoked_Then_It_Should_Return_Title()
{
// Arrange
var requestUri = "http://localhost:7071/api/openapi/v3.json";
// Act
var response = await this._http.GetStringAsync(requestUri).ConfigureAwait(false);
var doc = JsonConvert.DeserializeObject<OpenApiDocument>(response);
// Assert
doc.Should().NotBeNull();
doc.Info.Title.Should().Be("OpenAPI Document on Azure Functions");
doc.Components.Schemas.Should().ContainKey("greeting");
var schema = doc.Components.Schemas["greeting"];
schema.Type.Should().Be("object");
schema.Properties.Should().ContainKey("message");
var property = schema.Properties["message"];
property.Type.Should().Be("string");
}
}
For more information please refer the below links:-
SO THREAD:- System.InvaliOperationException: The gRPC channel URI 'http://0' could not be parsed .
MICROSOFT DOCUMENTATION:- Guide for running C# Azure Functions in an isolated process
I am in the process of adding integration tests at work for an MVC app. Many of our endpoints have policies applied to them, e.g.
namespace WorkProject
{
[Route("A/Route")]
public class WorkController : Controller
{
[HttpPost("DoStuff")]
[Authorize(Policy = "CanDoStuff")]
public IActionResult DoStuff(){/* */}
}
}
For our integration tests, I have overridden the WebApplicationFactory like it is suggested in the ASP .NET Core documentation. My goal was to overload the authentication step and to bypass the policy by making a class which allows all parties through the authorization policy.
namespace WorkApp.Tests
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
options.DefaultChallengeScheme = "Test Scheme";
}).AddTestAuth(o => { });
services.AddAuthorization(options =>
{
options.AddPolicy("CanDoStuff", policy =>
policy.Requirements.Add(new CanDoStuffRequirement()));
});
// I've also tried the line below, but neither worked
// I figured that maybe the services in Startup were added before these
// and that a replacement was necessary
// services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
services.Replace(ServiceDescriptor.Transient<IAuthorizationHandler, CanDoStuffActionHandler>());
});
}
}
internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
{
public CanDoStuffActionHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
internal class CanDoStuffRequirement : IAuthorizationRequirement
{
}
}
The first thing that I do to the services is override the authentication as suggested here (without the bit about overriding Startup since that didn't seem to work for me). I am inclined to believe that this authentication override works. When I run my tests, I receive an HTTP 403 from within the xUnit testing framework. If I hit the route that I am testing from PostMan I receive an HTTP 401. I have also made a class that lives in the custom web application factory that allows all requests for the CanDoStuff authorization handler. I thought this would allow the integration tests through the authorization policy, but, as stated above, I receive an HTTP 403. I know that a 403 will be returned if the app doesn't know where certain files are. However, this is a post route strictly for receiving and processing data and this route does not attempt to return any views so this 403 is most likely related to the authorization policy which, for some reason, is not being overridden.
I'm clearly doing something wrong. When I run the test under debug mode and set a breakpoint in the HandleRequirementsAsync function, the application never breaks. Is there a different way that I should be attempting to override the authorization policies?
Here is what I did.
Override the WebApplicationFactory with my own. Note, I still added my application's startup as the template parameter
Create my on startup function which overrides the ConfigureAuthServices function that I added.
Tell the builder in the ConfigureWebHost function to use my custom startup class.
Override the authentication step in the ConfigureWebHost function via builder.ConfigureServices.
Add an assembly reference to the controller whose endpoint I am trying to hit at the end of builder.ConfigureServices in the ConfigureWebHost function.
Write my own IAuthorizationHandler for the policy that allows all requests to succeed.
I hope I have done a decent job at explaining what I did. If not, hopefully the sample code below is easy enough to follow.
YourController.cs
namespace YourApplication
{
[Route("A/Route")]
public class WorkController : Controller
{
[HttpPost("DoStuff")]
[Authorize(Policy = "CanDoStuff")]
public IActionResult DoStuff(){/* */}
}
}
Test.cs
namespace YourApplication.Tests
{
public class Tests
: IClassFixture<CustomWebApplicationFactory<YourApplication.Startup>>
{
private readonly CustomWebApplicationFactory<YourApplication.Startup> _factory;
public Tests(CustomWebApplicationFactory<YourApplication.Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task SomeTest()
{
var client = _factory.CreateClient();
var response = await client.PostAsync("/YourEndpoint");
response.EnsureSuccessStatusCode();
Assert.Equal(/* whatever your condition is */);
}
}
}
CustomWebApplicationFactory.cs
namespace YourApplication.Tests
{
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
options.DefaultChallengeScheme = "Test Scheme";
}).AddTestAuth(o => { });
services.AddAuthorization(options =>
{
options.AddPolicy("CanDoStuff", policy =>
policy.Requirements.Add(new CanDoStuffRequirement()));
});
services.AddMvc().AddApplicationPart(typeof(YourApplication.Controllers.YourController).Assembly);
services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
});
builder.UseStartup<TestStartup>();
}
}
internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
{
public CanDoStuffActionHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
internal class CanDoStuffRequirement : IAuthorizationRequirement
{
}
}
TestStartup.cs
namespace YourApplication.Tests
{
public class TestStartup : YourApplication.Startup
{
public TestStartup(IConfiguration configuration) : base(configuration)
{
}
protected override void ConfigureAuthServices(IServiceCollection services)
{
}
}
}
Setup
Windows 10
Visual Studio Professional 2017 v15.9.9
ASP.NET Core 2.2
EF Core 2.2
Dapper
xUnit 2.4.1
Description
I'm using the WebApplicationFactory from the Microsoft.AspNetCore.Mvc.Testing package to setup my integration tests.
I've been following the official documentation to customise the web host configuration.
The SUT uses Dapper to query from the database, so I'm not using the In-Memory provider that ships with EF Core for this particular integration test.
My code for setting up the WebApplictionFactory is below:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseStartup<TStartup>()
.ConfigureServices(services =>
{
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database context
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<MyDbContext>(); // <-- service not found
dbContext.Database.EnsureCreated();
new MyDbContextSeed()
.SeedAsync(dbContext)
.Wait();
}
});
}
}
Issue
The MyDbContext service isn't found, I understand why (I think) - because the ServiceProvider from my Startup.cs class hasn't built yet.
But the question is How can I access the services from my Startup class here?
For context, the Integration test looks like this:
public class MyAPITests
: IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public MyAPITests(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task Get_ItemAsync_WhenIdNotFound_ReturnsNotFoundStatusCode()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/item/0");
// Act
var response = await _client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
First, ConfigureServices can be used instead of UseStartup, not together. Second, you shouldn't create the scope and do the migration during ConfigureServices anyways, but rather after the Web Host is built, see here:
In older tutorials, you may see similar code in the Configure method
in Startup.cs. We recommend that you use the Configure method only to
set up the request pipeline. Application startup code belongs in the
Main method.
The only way to do this is not in the factory, but after the factory built it:
public class MyAPITests
: IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public MyAPITests(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
var host = factory.Server.Host;
using (var scope = host.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<MyDbContext>();
dbContext.Database.EnsureCreated();
new MyDbContextSeed()
.SeedAsync(dbContext)
.Wait();
}
}
//...
}
Currently working on a web services project for my class and have decided to make a web API using .NET Core and DynamodDB.
I was just curious what the best way to inject the DynamoDBContext is?
I currently am doing it like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();
}
I got this piece of code above from the DynamoDB documentation. I add an instance of IAmazonDynamoDB to the project.
DynamoDBContext context;
public ValuesController(IAmazonDynamoDB context)
{
this.context = new DynamoDBContext(context);
}
In the controller, I then inject the IAmazonDynamoDB instance, and use that to create an instance of DynamoDBContext.
Is there a way to create an instance of the context in the ConfigureServices method and add it to the project there, or is the way I am doing it currently fine?
Is there a way to create an instance of the context in the
ConfigureServices method and add it to the project there, or is the
way I am doing it currently fine?
Although your solution will work, it has a drawback. You're not using Dependency Injection for DynamoDBContext and create its instance in controller constructor through new operator. You'll face a problems when it comes to unit testing your code, because you have no way to substitute implementation of DynamoDBContext.
The proper way is to register DynamoDBContext in DI container and let the container itself create an instance when it's required. With such approach IDynamoDBContext gets injected into ValuesController:
public class ValuesController
{
private readonly IDynamoDBContext context;
public ValuesController(IDynamoDBContext context)
{
this.context = context;
}
// ...
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();
services.AddTransient<IDynamoDBContext, DynamoDBContext>();
}
Basically you'd have to create an interface for your DynamoDB context
public interface IDynamoDbContext<T> : IDisposable where T : class
{
Task<T> GetByIdAsync(string id);
Task SaveAsync(T item);
Task DeleteByIdAsync(T item);
}
Create a class implementing the interface
public class DynamoDbContext<T> : DynamoDBContext, IDynamoDbContext<T>
where T : class
{
public DynamoDbContext(IAmazonDynamoDB client)
: base(client)
{
}
public async Task<T> GetByIdAsync(string id)
{
return await base.LoadAsync<T>(id);
}
public async Task SaveAsync(T item)
{
await base.SaveAsync(item);
}
public async Task DeleteByIdAsync(T item)
{
await base.DeleteAsync(item);
}
}
Inject it in your Startup like this
var client = Configuration.GetAWSOptions().CreateServiceClient<IAmazonDynamoDB>();
services.AddScoped<IDynamoDbContext<AwesomeClass>>(provider => new DynamoDbContext<AwesomeClass>(client));
The context will be passed in the DI system and you can use it where you like
private IDynamoDbContext<AwesomeClass> _awesomeContext;
public AwesomeDynamoDbService(IDynamoDbContext<AwesomeClass> awesomeContext)
{
_awesomeContext= awesomeContext;
}
I faced a similar issue and wrote a blog post describing a good way to fix it! Hope it shares some light!
This is how I am using it to make it work with both local & public (AWS hosted) DynamoDB.
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AWS": {
"Profile": "default",
"Region": "ap-south-1"
},
"DynamoDb": {
"LocalMode": false,
"LocalServiceUrl": "http://localhost:8001",
"TableNamePrefix": ""
}
}
Program.cs in .NET 6
// Get the AWS profile information from configuration providers
AWSOptions awsOptions = builder.Configuration.GetAWSOptions();
// Configure AWS service clients to use these credentials
builder.Services.AddDefaultAWSOptions(awsOptions);
var dynamoDbConfig = builder.Configuration.GetSection("DynamoDb");
var runLocalDynamoDb = dynamoDbConfig.GetValue<bool>("LocalMode");
#region DynamoDB setup
if (runLocalDynamoDb)
{
builder.Services.AddSingleton<IAmazonDynamoDB>(sp =>
{
var clientConfig = new AmazonDynamoDBConfig { ServiceURL = dynamoDbConfig.GetValue<string>("LocalServiceUrl") };
return new AmazonDynamoDBClient(clientConfig);
});
}
else
{
builder.Services.AddAWSService<IAmazonDynamoDB>();
}
builder.Services.AddSingleton<IDynamoDBContext, DynamoDBContext>((serviceProvider) =>
{
IAmazonDynamoDB amazonDynamoDBClient = serviceProvider.GetRequiredService<IAmazonDynamoDB>();
DynamoDBContextConfig dynamoDBContextConfig = new DynamoDBContextConfig
{
TableNamePrefix = dynamoDbConfig.GetValue<string>("TableNamePrefix")
};
return new DynamoDBContext(amazonDynamoDBClient, dynamoDBContextConfig);
});
#endregion
If you are using Lambda then you can try ti use below code
In your DynamoDB database class add constructor with dependency on IDynamoDBContext
public DynamoDbDatabase(IDynamoDBContext dynamoDbContext)
{
_dynamoDbContext = dynamoDbContext;
}
In Function.cs of your Lambda define mapping for dependency injection
private static IServiceProvider ConfigureServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IDynamoDBContext,DynamoDBContext>(p => new DynamoDBContext(new AmazonDynamoDBClient()));
return serviceCollection.BuildServiceProvider();
}
Call above function on top of your lambda function handler
when your code runs it will automatically detect dependency and pass proper DynamoDBContext object when asked for IDynamoDBContext