How do I bypass a Custom Attribute when Integration Testing? - c#

Basically, I would like to do a clean Integration Testing using net6.0. I want to invoke the endpoints of my controller which will eventually go into the database and write some data. Afterward, I will assert some aspects of the inserted data.
I can bypass the Bearer Token used by [Authorize] attribute but cannot bypass my custom attribute named as [HasPermission]
I have this custom attribute:
public class HasPermission : TypeFilterAttribute
{
public HasPermission(string resource, string scope)
: base(typeof(HasPermissionAuthorize))
{
Arguments = new object[] { resource, scope };
}
};
public class HasPermissionAuthorize : IAuthorizationFilter
{
private readonly string _resource;
private readonly string _scope;
private readonly IAuthService _authService;
public HasPermissionAuthorize(string resource, string scope, IAuthService authService)
{
_resource = resource;
_scope = scope;
_authService = authService;
}
public async void OnAuthorization(AuthorizationFilterContext context)
{
string token = context.HttpContext.Request.Headers["Authorization"];
token = token.Replace("Bearer ", "");
bool hasPermission = await _authService.HasClaimAsync(token, "permission", _resource + "." + _scope);
if (!hasPermission)
{
context.Result = new UnauthorizedResult();
}
}
And this is my controller:
[Authorize] //<-- I can bypass this
[Route("v1/[controller]")]
[ApiController]
public class MachinesController : ControllerBase
{
[HasPermission("Machines", "Create")] //<-- How do I bypass this?
[HttpPost]
public async Task<IActionResult> Post([FromBody] AddMachineRequest request)
{
//some logic
}
}
In my Integration Testing
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
//I'm bypassing the [Authorize] attribute here
//However, I do not know how to bypass [HasPermission] attribute
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Test")
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Test";
options.DefaultChallengeScheme = "Test";
options.DefaultScheme = "Test";
}) //TestAuthHandler is written somewhere else
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
}
}
}
I'm testing my code using IClassFixture<ApiWebApplicationFactory>. Even though I can bypass [Authorize] attribute, I could NOT find an elegant way to bypass [HasPermission] attribute.

OK. I already was thinking about Mocking IAuthService but as also Guru Stron pointed out, I went for that approach. So, here is my latest update on the code that solves my problem:
public class ApiWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
//Added this line
services.AddTransient<IAuthService>(_ => _authServiceMock.Object);
//Above somewhere I also mocked it like this:
//_authServiceMock.Setup(c => c.HasClaimAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(true);
//other stuff from my original question remains here and they are the same
}
}
}

Related

Integration tests do not recognize actions of controllers

I am trying integration tests with XUnit. I can access the endpoints that have been assigned on MapGet but to not actions inside controllers. I get a NotFound error. I am running the application on an API project with ApplicationFactory : WebApplicationFactory but all requests that I make with Factory.Client return NotFound. Is there any definition needed in integration tests in order to be able to send requests to controllers?
Test projects code:
private ApplicationFactory Factory { get; }
public AccountsControllerTests(ITestOutputHelper output)
{
Factory = new ApplicationFactory(output);
}
[Fact]
public async Task ListBalancesTest()
{
var client = Factory.CreateClient(new WebApplicationFactoryClientOptions(){AllowAutoRedirect = false});;
var resp = await client.GetAsync($"api/v1/test/get");
//!!!!!!! resp.StatusCode is HttpStatusCode.NotFound !!!!!!!
var mapgetResp= await client.GetAsync($"/test");
//!!!!!!! mapgetResp is Working !!!!!!!
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
API Code:
[ApiController]
[Route("api/v1/test")]
public class TestController : ControllerBase
{
[HttpGet("get")]
public async Task<ActionResult> Get()
{
return await Task.FromResult(Ok("Response from test"));
}
}
ApplicationFactory Code:
public class ApplicationFactory : WebApplicationFactory<TestStartup>
{
public ApplicationFactory(ITestOutputHelper testOutput = null)
{
_testOutput = testOutput;
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
var builder = WebHost
.CreateDefaultBuilder()
.UseEnvironment("Development")
.UseStartup<TestStartup>()
.UseSerilog();
if (_testOutput != null)
{
builder = builder.ConfigureLogging(logging =>
{
logging.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider>(new TestOutputHelperLoggerProvider(_testOutput)));
});
}
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseContentRoot(".");
builder.ConfigureServices(services =>
{/...../}
}
}
Startup:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/test", async context =>
{
await context.Response.WriteAsync(#$"Test value from MapGet");
});
/..../
});
Add code in API project in ConfigureWebHost(IWebHostBuilder builder)
builder.UseEnvironment("Test");
builder.ConfigureAppConfiguration((ctx, b) =>
{
ctx.HostingEnvironment.ApplicationName = typeof(Program).Assembly.GetName().Name;
});
builder.ConfigureServices(services =>
{
services.AddMvc()
.AddApplicationPart(typeof(TestStartup).Assembly); //TestStartup is Test project's startup!!
}
And works now thanks!

How to configure MassTransit with Mediator to publish messages?

I'm new with MassTransit and Mediator, I have a series of events to execute in consecutive order, I'm using MassTransit in-process and in-memory, for my use case no transport is required.
I want to send and publish messages to consumers, sagas, activities through Mediator, I have the code below, but I want to improve it by registering MassTransit in startup.cs:
//asp net core 3.1 Controller
[ApiController]
public class MyController : ControllerBase
{
private readonly IProductService _productService ;
private readonly IMediator mediator;
public MyController(IProductService productService)
{
_productService = productService;
var repository = new InMemorySagaRepository<ApiSaga>();
mediator = Bus.Factory.CreateMediator(cfg =>
{
cfg.Saga<ProductSaga>(repository);
});
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] ProductContract productContract)
{
try
{
var result = await _productService.DoSomeThingAsync(productContract);
await mediator.Publish<ProductSubmittedEvent>(new { CorrelationId = Guid.NewGuid(), result.Label });
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
//My saga
public class ProductSaga :
ISaga,
InitiatedBy<ProductSubmittedEvent>
{
public Guid CorrelationId { get; set; }
public string State { get; private set; } = "Not Started";
public Task Consume(ConsumeContext<ProductSubmittedEvent> context)
{
var label= context.Message.Label;
State = "AwaitingForNextStep";
//...
//send next command
}
}
Like this it works but it's not proper, I want to configure masstransit with Mediator in my startup.cs to have one proper instance, to do that I started by deleting the IMediator, using an IPublishEndpoint to publish messages to Saga and configuring my startup.cs, but it doesn't work as expected:
//startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMediator(cfg =>
{
cfg.AddSaga<ProductSaga>().InMemoryRepository();
});
}
//in controller using:
private readonly IPublishEndpoint _publishEndpoint;
//then
await _publishEndpoint.Publish<ProductSubmittedEvent>(
new { CorrelationId = Guid.NewGuid(), result.Label });
I got a System.InvalidOperationException:
Unable to resolve service for type 'MassTransit.IPublishEndpoint' while attempting to activate 'GaaS.API.Controllers.ManageApiController'.
I tried to update my startup.cs:
var repository = new InMemorySagaRepository<ApiSaga>();
services.AddMassTransit(cfg =>
{
cfg.AddBus(provider =>
{
return Bus.Factory.CreateMediator(x =>
{
x.Saga<ProductSaga>(repository);
});
});
});
I got:
Cannot implicitly convert type 'MassTransit.Mediator.IMediator' to 'MassTransit.IBusControl'.
If you have any recommendation ideas thanks for sharing and challenging me 😊
The proper way to configure MassTransit Mediator in your project is through the Startup.cs file, which you seem to have tried.
public void ConfigureServices(IServiceCollection services)
{
services.AddMediator(cfg =>
{
cfg.AddSaga<ProductSaga>().InMemoryRepository();
});
}
Using mediator, you need to depend upon the IMediator interface. You cannot use IPublishEndpoint or ISendEndpointProvider, as those are bus interfaces. Since you can have both mediator and a bus instance in the container at the same time, this would lead to confusion when resolving services from the container.
[ApiController]
public class MyController : ControllerBase
{
private readonly IProductService _productService ;
private readonly IMediator _mediator;
public MyController(IProductService productService, IMediator mediator)
{
_productService = productService;
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] ProductContract productContract)
{
try
{
var result = await _productService.DoSomeThingAsync(productContract);
await _mediator.Publish<ProductSubmittedEvent>(new { CorrelationId = NewId.NextGuid(), result.Label });
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
If you are only using mediator, and want to use IPublishEndpoint, you could add that to the container yourself and delegate it.
services.AddSingleton<IPublishEndpoint>(provider => provider.GetService<IMediator>());
I got to this from the (excellent) youtube video - MassTransit starting with Mediator, in that sample there is a line of code
AddMediator()
which I couldn't locate. I believe the following setup provides everything needed to get code working based on that video...
services.AddMassTransit(config =>
{
config.AddRequestClient<ISubmitOrder>();
config.AddConsumersFromNamespaceContaining<SubmitOrderConsumer>();
config.UsingInMemory(ConfigureBus);
});
and ConfigureBus is then:
private void ConfigureBus(IBusRegistrationContext context, IInMemoryBusFactoryConfigurator configurator)
{
configurator.ConfigureEndpoints(context);
}
I couldn't readily find this elsewhere, hence posting here.

How save globally catched exceptions in database

I am building a web application where I will have a lot of controllers with their corresponding action methods in them.
I want to save every exception in database and for this reason I have created
ExceptionService (DbContext is injected in it).
let's say that this is the general form of my controllers:
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly UserManager userManager;
private readonly IExceptionService exceptionService;
public UserController(UserManager userManager, IExceptionService exceptionService)
{
this.userManager = userManager;
this.exceptionService = exceptionService;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] User user)
{
try
{
//some code
}
catch (Exception e)
{
exceptionService.Save(e);
//some code
}
}
}
In order to avoid so many try-catch blocks I decided to create a filter which looks like this:
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IExceptionService exceptionService;
public ApiExceptionFilterAttribute(IExceptionService exceptionService)
{
this.exceptionService = exceptionService;
}
public override void OnException(ExceptionContext context)
{
Exception e = context.Exception;
exceptionService.Save(e);
//some code
}
}
Code in ConfigureServices method in StartUp.cs looks like this (some code removed for simplicity):
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services
.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddScoped<UserManager>();
services.AddScoped<SignInManager>();
services.AddScoped<IExceptionService, ExceptionService>();
services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
ConfgureMvcOptions class looks like this:
public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly IExceptionService exceptionService;
public ConfigureMvcOptions(IExceptionService exceptionService)
{
this.exceptionService = exceptionService;
}
public void Configure(MvcOptions options)
{
options.Filters.Add(new ApiExceptionFilterAttribute(exceptionService));
}
}
When I run this application, I get the following error:
System.InvalidOperationException: 'Cannot consume scoped service 'SmartWay.Services.IExceptionService' from singleton 'Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.AspNetCore.Mvc.MvcOptions]'.'
If I change IExceptionServcise's lifetime to transient than I have to do so for
Dbcontext, then for DbContextOptions... It seems that it isn't right way..
So, How can I solve this problem?
For resolving scoped service from singleton service, try _serviceProvider.CreateScope.
Follow steps below:
ExceptionService
public interface IExceptionService
{
void Save(Exception ex);
}
public class ExceptionService : IExceptionService
{
private readonly IServiceProvider _serviceProvider;
public ExceptionService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Save(Exception ex)
{
using (var scope = _serviceProvider.CreateScope())
{
var _context = scope.ServiceProvider.GetRequiredService<MVCProContext>();
_context.Add(new Book() { Title = ex.Message });
_context.SaveChanges();
}
}
}
Startup.cs
services.AddSingleton<IExceptionService, ExceptionService>();
services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();

Access HttpContextAccessor from Helper Class in .net core web api

I want to access JwtHelper from ExceptionHelper. But problem is ExceptionHelper must be static. And so, we can't create constructor and not access jwtHelper Method. How can I achieve access jwHelper from ExcewptionHelper.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<MyDbContext>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
context.Response.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
ExceptionHelper.cs
public static class ExceptionHelper
{
public static async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = jwtHelper.GetValueFromToken(token, "UserId");??????
//in this line I can't access jwtHelper.
}
}
JwtHelper.cs
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string stream, string propertyName)
{
var jwt = httpContextAccessor.HttpContext.Request.Headers["Authorization"];
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(stream.Replace("Bearer ", "")) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
If I were you I would register JwtHelper with a Interface known as IJwtHelper.
It would look like this then
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string propertyName)
{
var jwt= httpContextAccessor.HttpContext.Request.Headers["Authorization"];
// I can't access httpContextAccessor in this line.
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(jwt) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
public interface IJwtHelper
{
string GetValueFromToken(string propertyName);
}
In my startup.cs class I would then do
services.AddSingleton<IJwtHelper, JwtHelper>();
And then when you want to access your helper I would inject IJwtHelper
private IJwtHelper _jwtHelper;
public SomeConstructerOnClass(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public void SomeMethod(string property) {
var token = _jwtHelper.GetValueFromToken(property);
//Do something with token
}
where _jwtHelper is field of type IJwtHelper.
You will then be able to use GetValueFromToken quite fine anywhere you inject IJwtHelper
UPDATE
Your problem is that ExceptionHandler is Static , implement an interface and add it to container
public class ExceptionHelper : IExceptionHelper
{
private IJwtHelper _jwtHelper;
public ExceptionHelper(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = _jwtHelper.GetValueFromToken(token, "UserId");??????
}
}
public interface IExceptionHelper
{
Task AddApplicationError( HttpResponse response);
}
Then
services.AddSingleton<IExceptionHelper, ExceptionHelper>();
Now You will be able to inject it into your Configure method like so
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IExceptionHelper exceptionHelper)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
//Resolved and available!
exceptionHelper.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
If you follow me advice above from my initial response and my update everything should be fine and registered nicely in your container :)
You'll have to instantiate the JwtHelper class in order to access the instance variable (httpContextAccessor) from another class. Static methods, like GetValueFromToken, cannot access instance variables.

Strong-type OpenIdConnectOptions for aspnet core 2.0

I am trying to understand How can we utilize the strong-type when constructing the OpenIDConnectOptions.
I know we can implement strong type for appsettings and other items using POCO class and IOptions implementation and accessing those from Controller constructor But here, my issue is before the controller part. At run-time starup it fails.
To Start with, I have startup.configureservice with:
services.AddAzureADOpenIDAuthentication(Configuration);
I have Extension method for IServiceCollection for AddAzureADOpenIDAuthentication like:
services.Configure<AzureADOptions>(configuration.GetSection("Authentication:AzureAd"));
services.AddSingleton<IOptionsMonitor<OpenIdConnectOptions>, AzureADOpenIdConnectOptionsSetup>();
services.AddAuthentication(auth =>
{
auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie().AddOpenIdConnect();
return services;
Finally I have AzureADOpenIdConnectOptionsSetup with the implementation of IOptionsMonitor like below:
public class AzureADOpenIdConnectOptionsSetup : IOptionsMonitor<OpenIdConnectOptions>
{
public OpenIdConnectOptions CurrentValue { get; set; }
public AzureADOpenIdConnectOptionsSetup(IOptionsMonitor<AzureADOptions> azureADOptions)
{
CurrentValue = new OpenIdConnectOptions();
CurrentValue.ClientId = azureADOptions.CurrentValue.ClientId;
CurrentValue.Authority = azureADOptions.CurrentValue.Authority;
CurrentValue.CallbackPath = azureADOptions.CurrentValue.CallbackPath;
}
public OpenIdConnectOptions Get(string name)
{
return CurrentValue;
}
public IDisposable OnChange(Action<OpenIdConnectOptions, string> listener)
{
throw new NotImplementedException();
}
}
When I run this code, It hit the Constructor and OpenIdConnectOptions Get twice and through the breakpoint at constructor level, I check the settings are correctly transferred from azureADOptions to OpenIdConnectOptions CurrentValue.
Still, I am getting an error message (before I press login, it means startup it self)
InvalidOperationException: Provide Authority, MetadataAddress, Configuration, or ConfigurationManager to OpenIdConnectOptions
I am not sure, whether I have correctly implemented OpenIdConnectOptions Get(string name) or not.
One more doubt is, How should I implement OnChange(Action listener) to listen the run-time change of appsettings.json
For returning OpenIdConnectOptions, you need to initialize ConfigurationManager, and a simple code like below:
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureADOpenIDAuthentication(this AuthenticationBuilder builder, IConfiguration configuration)
{
builder.Services.Configure<AzureAdOptions>(configuration.GetSection("AzureAd"));
builder.Services.AddSingleton<IOptionsMonitor<OpenIdConnectOptions>, AzureADOpenIdConnectOptionsSetup>();
builder.Services.AddAuthentication(auth =>
{
auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect();
return builder;
}
public class AzureADOpenIdConnectOptionsSetup : IOptionsMonitor<OpenIdConnectOptions>
{
public OpenIdConnectOptions CurrentValue { get; set; }
private IDataProtectionProvider _dataProtectionProvider;
public AzureADOpenIdConnectOptionsSetup(IOptionsMonitor<AzureAdOptions> azureADOptions,IDataProtectionProvider dataProtectionProvider)
{
_dataProtectionProvider = dataProtectionProvider;
CurrentValue = new OpenIdConnectOptions
{
ClientId = azureADOptions.CurrentValue.ClientId,
Authority = $"{azureADOptions.CurrentValue.Instance}{azureADOptions.CurrentValue.TenantId}",
CallbackPath = azureADOptions.CurrentValue.CallbackPath
};
}
public OpenIdConnectOptions Get(string name)
{
OpenIdConnectPostConfigureOptions op = new OpenIdConnectPostConfigureOptions(_dataProtectionProvider);
op.PostConfigure(name, CurrentValue);
return CurrentValue;
}
public IDisposable OnChange(Action<OpenIdConnectOptions, string> listener)
{
throw new NotImplementedException();
}
}
}

Categories