How use JWT Bearer Scheme when working with WebApplicationFactory - c#

I have a working web API that I recently updated to use JWT auth and, while it is working when I run it normally, I can't seem to get my integration tests to work.
I wanted to start working on integrating a token generation option for my integration tests, but I can't even get them to throw a 401.
When I run any of my preexisting integration tests that work without JWT in the project, I'd expect to get a 401 since I don't have any auth info, but am actually getting a System.InvalidOperationException : Scheme already exists: Bearer error.
I'd assume this is happening because of the way that WebApplicationFactory works by running its ConfigureWebHost method runs after the Startup class' ConfigureServices method and when i put a breakpoint on my jwt service, it does indeed get hit twice, but given that this is how WebApplicationFactory is built, I'm not sure what the recommended option here is. Of note, even when I remove one of the services I still get the error:
var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(JwtBearerHandler));
services.Remove(serviceDescriptor);
My WebApplicationFactory is based on the eshopwebapi factory:
public class CustomWebApplicationFactory : WebApplicationFactory<StartupTesting>
{
// checkpoint for respawning to clear the database when spinning up each time
private static Checkpoint checkpoint = new Checkpoint
{
};
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(async services =>
{
// Create a new service provider.
var provider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (LabDbContext) using an in-memory
// database for testing.
services.AddDbContext<LabDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(provider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<LabDbContext>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
await checkpoint.Reset(db.Database.GetDbConnection());
}
catch
{
}
}
}).UseStartup<StartupTesting>();
}
public HttpClient GetAnonymousClient()
{
return CreateClient();
}
}
This is my service registration:
public static class ServiceRegistration
{
public static void AddIdentityInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["JwtSettings:Authority"];
options.Audience = configuration["JwtSettings:Audience"];
});
services.AddAuthorization(options =>
{
options.AddPolicy("CanReadPatients",
policy => policy.RequireClaim("scope", "patients.read"));
options.AddPolicy("CanAddPatients",
policy => policy.RequireClaim("scope", "patients.add"));
options.AddPolicy("CanDeletePatients",
policy => policy.RequireClaim("scope", "patients.delete"));
options.AddPolicy("CanUpdatePatients",
policy => policy.RequireClaim("scope", "patients.update"));
});
}
}
And this is my integration test (that I would expect to currently throw a 401):
public class GetPatientIntegrationTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly CustomWebApplicationFactory _factory;
public GetPatientIntegrationTests(CustomWebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetPatients_ReturnsSuccessCodeAndResourceWithAccurateFields()
{
var fakePatientOne = new FakePatient { }.Generate();
var fakePatientTwo = new FakePatient { }.Generate();
var appFactory = _factory;
using (var scope = appFactory.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<LabDbContext>();
context.Database.EnsureCreated();
context.Patients.AddRange(fakePatientOne, fakePatientTwo);
context.SaveChanges();
}
var client = appFactory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var result = await client.GetAsync("api/Patients")
.ConfigureAwait(false);
var responseContent = await result.Content.ReadAsStringAsync()
.ConfigureAwait(false);
var response = JsonConvert.DeserializeObject<Response<IEnumerable<PatientDto>>>(responseContent).Data;
// Assert
result.StatusCode.Should().Be(200);
response.Should().ContainEquivalentOf(fakePatientOne, options =>
options.ExcludingMissingMembers());
response.Should().ContainEquivalentOf(fakePatientTwo, options =>
options.ExcludingMissingMembers());
}
}

Hey I saw your post when I was looking for the same answer. I solved it by putting the following code in the ConfigureWebHost method of my WebApplicationFactory:
protected override void ConfigureWebHost(
IWebHostBuilder builder)
{
builder.ConfigureServices(serviceCollection =>
{
});
// Overwrite registrations from Startup.cs
builder.ConfigureTestServices(serviceCollection =>
{
var authenticationBuilder = serviceCollection.AddAuthentication();
authenticationBuilder.Services.Configure<AuthenticationOptions>(o =>
{
o.SchemeMap.Clear();
((IList<AuthenticationSchemeBuilder>) o.Schemes).Clear();
});
});
}
I know I'm four months late, but I hope you still have any use for it.

Related

im getting following error when tryng to Authenticate my app through azure AD Multiple tenant users

Error :
InvalidOperationException: IDX20803: Unable to obtain configuration from: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
i have registered my API and web application both as multitenant but im not able to log in through another tenant,when im accessing the API through the app using the same tenant user in which app and api is registered it is letting me login,but cant get it done with multiple tenant.
when im adding instance as
"Instance": "https://login.microsoftonline.com",
it is working fine.
but when i add instance as
"Instance": "https://login.microsoftonline.com/common",
it throws error.
here is the code i have written inside startup.cs of web application
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddHttpClient<ITokenService, TokenService>();
services.AddMicrosoftIdentityWebAppAuthentication(Configuration).EnableTokenAcquisitionToCallDownstreamApi(new string[]
{ Configuration["APIConfig:APIScope"]}).AddInMemoryTokenCaches();
services.AddControllersWithViews(option =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
option.Filters.Add(new AuthorizeFilter(policy));
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// Instead of using the default validation (validating against a single issuer value, as we do in
// line of business apps), we inject our own multitenant validation logic
ValidateIssuer = false,
// If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
//IssuerValidator = (issuer, securityToken, validationParameters) => {
// if (myIssuerValidationLogic(issuer)) return issuer;
//}
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
// If your authentication logic is based on users then add your logic here
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Error");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
// If your application needs to authenticate single users, add your user validation below.
//OnTokenValidated = context =>
//{
// return myUserValidationLogic(context.Ticket.Principal);
//}
};
});
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddRazorPages();
}
this is the code for aquiring token
public interface ITokenService
{
public Task<string> Get();
}
public class TokenService : ITokenService
{
private readonly HttpClient _httpClient;
private readonly string _APIScope = string.Empty;
private readonly string _APIBaseAddress = string.Empty;
private readonly ITokenAcquisition _tokenAquisition;
public TokenService(ITokenAcquisition tokenAquisition, HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_tokenAquisition = tokenAquisition;
_APIScope = configuration["APIConfig:APIScope"];
_APIBaseAddress = configuration["APIConfig:APIBaseaddress"];
}
public async Task<string> Get()
{
await FindToken();
var response = await _httpClient.GetAsync($"{_APIBaseAddress}/weatherforecast");
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var content = await response.Content.ReadAsStringAsync();
var output = JsonConvert.DeserializeObject<string>(content);
return output;
}
throw new HttpRequestException("Invalid Response");
}
private async Task FindToken()
{
var accessToken = await _tokenAquisition.GetAccessTokenForUserAsync(new[] { _APIScope });
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
}
Authority must be something like
"https://login.microsoftonline.com/common" which is combination of
instance and tenant Id.
So here instance must be https://login.microsoftonline.com
Also do check the comment here> IDX20803: Unable to obtain configuration from
Try changing your web app's minimum TLS version to 1.2 from 1.0.
In some cases packages maybe defaulting to TLS 1.1 even after that
when loading that metadata.
To resolve, I add the following in Global.asax.cs:
protected void Application_Start()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3; // allow TLSV1.2 and SSL3 only
//other code
}
You may need to check real error.As you have made IdentityModelEventSource.ShowPII = true; in start up.cs please check the logs for exact error .
if (env.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
}

How to use EF Core after I've received a message from my Azure Service bus listener/consumer, if my context is disposed?

I have a website Angular frontend and WebAPI on the backend with all my controllers, I also have a service (C# class) that I call as a singleton as a long running task to listen for incoming Azure service bus messages.
FYI - I can't pass any scoped services (DbContext) to a singleton (ServiceBusConsumer), so I can't pass in my DB context to this service.
QUESTION - Once I receive an incoming service bus message, how do I call up my DB and use it?
Here is my service listening for and receiving messages.
Startup.cs
services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
Program.cs -> in Main() I start the service
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
ServiceBusConsumer.cs
public class ServiceBusConsumer : IServiceBusConsumer
{
private readonly IConfiguration _config;
private readonly ServiceBusClient _queueClient;
private readonly ServiceBusProcessor _processor;
// private readonly DataContext _context;
public ServiceBusConsumer(IConfiguration config,
// DataContext context)
{
_config = config;
// _context = context;
_queueClient = new ServiceBusClient(_config["ServiceBus:Connection"]);
_processor = _queueClient.CreateProcessor(_config["ServiceBus:Queue"], new ServiceBusProcessorOptions());
}
public void RegisterOnMessageHandlerAndReceiveMessages() {
_processor.ProcessMessageAsync += MessageHandler;
_processor.ProcessErrorAsync += ErrorHandler;
_processor.StartProcessingAsync();
}
private async Task MessageHandler(ProcessMessageEventArgs args)
{
string body = args.Message.Body.ToString();
JObject jsonObject = JObject.Parse(body);
var eventStatus = (string)jsonObject["EventStatus"];
await args.CompleteMessageAsync(args.Message);
// _context is disposed
// want to connect to DB here but don't know how!
// var ybEvent = _context.YogabandEvents.Where(p => p.ServiceBusSequenceNumber == args.Message.SequenceNumber).FirstOrDefault();
}
private Task ErrorHandler(ProcessErrorEventArgs args)
{
var error = args.Exception.ToString();
return Task.CompletedTask;
}
}
Error
Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.\nObject name: 'DataContext'.
Here is Program.cs
public class Program
{
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<DataContext>();
var userManager = services.GetRequiredService<UserManager<User>>();
var roleManager = services.GetRequiredService<RoleManager<Role>>();
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occured during migration");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Here is Startup.cs -> just the ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper(typeof(MappingEvents));
services.AddAutoMapper(typeof(MappingMembers));
services.AddAutoMapper(typeof(MappingUsers));
services.AddAutoMapper(typeof(MappingYogabands));
services.AddAutoMapper(typeof(MappingReviews));
// objects being passed back to the UI. Before I was passing User/Photo/etc and they
// had loops/refrences back to the user objects
services.AddControllers().AddNewtonsoftJson(opt =>
{
opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Error;
});
services.AddDbContext<DataContext>(x =>
// x.UseSqlite(_config.GetConnectionString("DefaultConnection"), y => y.UseNetTopologySuite()));
x.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
services.Configure<AuthMessageSenderOptions>(_config.GetSection("SendGrid"));
services.Configure<AuthMessageSenderOptionsNew>(_config.GetSection("SendGrid"));
services.Configure<ConfirmationOptions>(_config.GetSection("Confirmation"));
services.Configure<CloudinarySettings>(_config.GetSection("CloudinarySettings"));
services.AddApplicationServices();
services.AddIdentityServices(_config);
services.AddSwaggerDocumentation();
services.AddCors(opt =>
{
opt.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200");
});
});
}
Here is AddApplicationServices()
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// scoped - better option when you want to maintain state within a request
// services.AddScoped<IEventConsumer, EventConsumer>();
services.AddScoped<IServiceBusProducer, ServiceBusProducer>();
services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
services.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<IEmailSender, EmailSenderNew>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>)));
services.AddScoped<LogUserActivity>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var errors = actionContext.ModelState
.Where(e => e.Value.Errors.Count > 0)
.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage).ToArray();
var errorResponse = new ApiValidationErrorResponse
{
Errors = errors
};
return new BadRequestObjectResult(errorResponse);
};
});
return services;
}
It seems your problem is with DI.
Your ServiceBusConsumer service is a singleton but you inject a DbContext as a constructor. This is generally the recommendation but in this case, it can't work.
You inject a DbContext in the constructor and "save" a "link" to it. But then it gets disposed, so that "link" won't work.
Instead, you should inject a DbContextFactory. With a factory, you can create DbContext instances on demand.
private readonly IDbContextFactory<DataContext> _contextFactory;
public ServiceBusConsumer(IConfiguration config, IDbContextFactory<DataContext> contextFactory)
{
// Add this line
_contextFactory = contextFactory;
}
private async Task MessageHandler(ProcessMessageEventArgs args)
{
// With the new C# 8 syntax you can do
using var db = _contextFactory.CreateDbContext();
// Otherwise, wrap it up
using (var db = _contextFactory.CreateDbContext())
{
}
}
Here's a link to a docs where they show how it can be used: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor
You just need to register it:
public void ConfigureServices(IServiceCollection services)
{
// Add this line to register a context factory
services.AddDbContextFactory<DataContext>(
options =>
.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
}
You can't use the same DI as with controllers, since they're usually not singletons, therefore won't run into this problem. AFAIK the DbContextFactory was created exactly for this purpose (with Blazor in mind). If the service you needed was not a DbContext you would need to inject the service provider in the constructor and then request the service directly, although Microsoft doesn't recommend that.
I solved same problem avoiding using statment, instead declare scope variable within Main task. You want to keep alive the scope created for queue message handlers so your Program.cs should be like this:
public class Program
{
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
// This variable is working within ServiceBus threads so it need to bee keept alive until Main ends
var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<DataContext>();
var userManager = services.GetRequiredService<UserManager<User>>();
var roleManager = services.GetRequiredService<RoleManager<Role>>();
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occured during migration");
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

Connect to Google api via asp .net core causes endless authorization loop

I am currently trying to set up an Asp .net core 3 web project to connect to Google calendar and request user data after the user has logged in.
The user logs in and the application requests permission to access their data. The problems start after that.
start up
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddControllersWithViews();
services.AddRazorPages();
services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
IConfigurationSection googleAuthSection = Configuration.GetSection("Authentication:Google");
options.ClientId = googleAuthSection["ClientId"];
options.ClientSecret = googleAuthSection["ClientSecret"];
options.Scope.Add(Google.Apis.Calendar.v3.CalendarService.Scope.Calendar);
});
}
controller
public class CalController : Controller
{
private readonly ILogger<CalController> _logger;
public CalController(ILogger<CalController> logger)
{
_logger = logger;
}
[Authorize]
public async Task<IActionResult> Index([FromServices] IGoogleAuthProvider auth)
{
var cred = await auth.GetCredentialAsync();
var service = new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = cred
});
var calendar = await service.Calendars.Get("primary").ExecuteAsync();
return View();
}
}
Issue
Currently the system is looping on me. When i navigate to the calendar controller It comes with the following error.
So i created an account controller with the following action.
public IActionResult Logins([FromQuery] string returnURL)
{
return RedirectToAction("Index", "Cal");
}
Which now just causes the whole thing to loop. Why isn't the authorize attribute detecting that it is logged in and authenticated?
strange thing
If I remove the authorize attribute. Login the user and go directly to the cal controller i have access to their data and it all works.
But as soon as i add the authorize attributed it cant detect that it is authenticated.
Google .net client library.
I originally posted this over on the Google .net client libary 1584 unfortunately the team was not able to assist in getting this to work with asp .net core even though it is supposed to work.
I suspect there is something wrong with my setup but i am at a lost to figure out what the issue could be.
I finally got this working.
startup.cs
public class Client
{
public class Web
{
public string client_id { get; set; }
public string client_secret { get; set; }
}
public Web web { get; set; }
}
public class ClientInfo
{
public Client Client { get; set; }
private readonly IConfiguration _configuration;
public ClientInfo(IConfiguration configuration)
{
_configuration = configuration;
Client = Load();
}
private Client Load()
{
var filePath = _configuration["TEST_WEB_CLIENT_SECRET_FILENAME"];
if (string.IsNullOrEmpty(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
if (!File.Exists(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
var x = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<Client>(File.ReadAllText(filePath));
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ClientInfo>();
services.AddControllers();
services.AddAuthentication(o =>
{
// This is for challenges to go directly to the Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This is for forbids to go directly to the Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
var clientInfo = new ClientInfo(Configuration);
options.ClientId = clientInfo.Client.web.client_id;
options.ClientSecret = clientInfo.Client.web.client_secret;
});
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
Controller with Auth
[ApiController]
[Route("[controller]")]
public class GAAnalyticsController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public GAAnalyticsController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
// Test showing use of incremental auth.
// This attribute states that the listed scope(s) must be authorized in the handler.
[GoogleScopedAuthorize(AnalyticsReportingService.ScopeConstants.AnalyticsReadonly)]
public async Task<GetReportsResponse> Get([FromServices] IGoogleAuthProvider auth, [FromServices] ClientInfo clientInfo)
{
var GoogleAnalyticsViewId = "78110423";
var cred = await auth.GetCredentialAsync();
var service = new AnalyticsReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = cred
});
var dateRange = new DateRange
{
StartDate = "2015-06-15",
EndDate = "2015-06-30"
};
// Create the Metrics object.
var sessions = new Metric
{
Expression = "ga:sessions",
Alias = "Sessions"
};
//Create the Dimensions object.
var browser = new Dimension
{
Name = "ga:browser"
};
// Create the ReportRequest object.
var reportRequest = new ReportRequest
{
ViewId = GoogleAnalyticsViewId,
DateRanges = new List<DateRange> {dateRange},
Dimensions = new List<Dimension> {browser},
Metrics = new List<Metric> {sessions}
};
var requests = new List<ReportRequest> {reportRequest};
// Create the GetReportsRequest object.
var getReport = new GetReportsRequest {ReportRequests = requests};
// Make the request.
var response = service.Reports.BatchGet(getReport).Execute();
return response;
}
}

asp.net core - Integration test and view components

I'm facing a strange issue since I created a view component in my app.
All my integration tests using an HttpClient start failing with response code "Internal Server Error".
Here the test configuration :
public class BaseTest<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly bool _hasUser;
private readonly HttpClient _client;
protected BaseTest(bool hasUser = false)
{
_hasUser = hasUser;
_client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
}
protected async Task<HttpResponseMessage> GetPageByPath(string path)
{
return await _client.GetAsync(path);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
if (_hasUser)
{
services.AddScoped<IAuthenticationService, AuthenticationServiceStub>();
services.AddSingleton<IStartupFilter, FakeUserFilter>();
services.AddMvc(options =>
{
options.Filters.Add(new AllowAnonymousFilter());
options.Filters.Add(new AuthenticatedAttribute());
});
}
});
builder.ConfigureServices(services =>
{
// Create a new service provider.
ServiceProvider serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var logger = scopedServices
.GetRequiredService<ILogger<BaseTest<TStartup>>>();
}
});
}
}
}
And a usage example :
public class BasicTest : BaseTest<Startup>
{
public BasicTest() : base()
{
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Users/SignOut")]
[Trait("Category", "Integration")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Act
var response = await GetPageByPath(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
If you need the component code let me know.
I already rollback code to check when this start happening, and it's start after the commit with the new Component called in several pages.

Overriding database provider in integration test with WebApplicationFactory

I am following the official MS documentation for integration testing .Net Core (https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1).
I was able to get the first part of the integration test done where I was not overriding the startup class of the application I am testing (i.e. I was using a web application factorythat did not override any services).
I want to override the database setup to use an in-memory database for the integration test. The problem I am running into is that the configuration continues to try and use the sql server for services.AddHangfire().
How do I override only above specific item in my integration test? I only want to override the AddHangfire setup and not services.AddScoped<ISendEmail, SendEmail>(). Any help would be appreciated.
Test Class with the custom web application factory
public class HomeControllerShouldCustomFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public HomeControllerShouldCustomFactory(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task IndexRendersCorrectTitle()
{
var response = await _client.GetAsync("/Home/Index");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Send Email", responseString);
}
}
Custom Web Application Factory
public class CustomWebApplicationFactory<TStartup>: WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My startup.cs in my application that I am testing
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ISendEmail, SendEmail>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
RecurringJob.AddOrUpdate<ISendEmail>((email) => email.SendReminder(), Cron.Daily);
app.UseMvc();
Update
I don't see this issue in my other example project where I am using only entity framework. I have a simple application with an application db context which uses SQL server. In my test class, I override it with an in-memory database and everything works. I am at a loss at to why it will work in my example application but not work in my main application. Is this something to do with how HangFire works?
In my test application (example code below), I can delete my sql database, run my test, and the test passes because the application DB context does not go looking for the sql server instance but uses the in-memory database. In my application, the HangFire service keeps trying to use the sql server database (if I delete the database and try to use an in-memory database for the test - it fails because it can't find the instance its trying to connect to). How come there is such a drastic difference in how the two projects work when a similar path is used for both?
I ran through the debugger for my integration test which calls the index method on the home controller above (using the CustomWebApplicationFactory). As I am initializing a test server, it goes through my startup class which calls below in ConfigureServices:
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
After that, the Configure method tries to call below statement:
app.UseHangfireServer();
At this point the test fails as It cannot find the DB. The DB is hosted on Azure so I am trying to replace it with an in-memory server for some of the integration test. Is the approach I am taking incorrect?
My example application where its working
Application DB Context in my example application
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Message> Messages { get; set; }
public async Task<List<Message>> GetMessagesAsync()
{
return await Messages
.OrderBy(message => message.Text)
.AsNoTracking()
.ToListAsync();
}
public void Initialize()
{
Messages.AddRange(GetSeedingMessages());
SaveChanges();
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "You're standing on my scarf." },
new Message(){ Text = "Would you like a jelly baby?" },
new Message(){ Text = "To the rational mind, nothing is inexplicable; only unexplained." }
};
}
}
Startup.cs in my example application
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
CustomWebApplicationFactory - in my unit test project
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My unit test in my unit test project
public class UnitTest1 : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public UnitTest1(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async System.Threading.Tasks.Task Test1Async()
{
var response = await _client.GetAsync("/");
//response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Home", responseString);
}
Update 2
I think I found an alternate to trying to override all my configuration in my integration test class. Since it's a lot more complicated to override HangFire as opposed to an ApplicationDBContext, I came up with below approach:
Startup.cs
if (Environment.IsDevelopment())
{
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
}
else
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration["DBConnection"]));
}
Then in my CustomWebApplicationBuilder, I override the environment type for testing:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development"); //change to Production for alternate test
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
});
}
}
With that approach, I don't need to worry about having to do extra logic to satisfy hangfire's check for an active DB. It works but I am not 100% convinced its the best approach as I'm introducing branching in my production startup class.
There are two different scenarios you need to check.
Create a job by class BackgroundJob
Create a job by interface IBackgroundJobClient
For the first option, you could not replace the SqlServerStorage with MemoryStorage.
For UseSqlServerStorage, it will reset JobStorage by SqlServerStorage.
public static IGlobalConfiguration<SqlServerStorage> UseSqlServerStorage(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] string nameOrConnectionString)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));
var storage = new SqlServerStorage(nameOrConnectionString);
return configuration.UseStorage(storage);
}
UseStorage
public static class GlobalConfigurationExtensions
{
public static IGlobalConfiguration<TStorage> UseStorage<TStorage>(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] TStorage storage)
where TStorage : JobStorage
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (storage == null) throw new ArgumentNullException(nameof(storage));
return configuration.Use(storage, x => JobStorage.Current = x);
}
Which means, no matter what you set in CustomWebApplicationFactory, UseSqlServerStorage will reset BackgroundJob with SqlServerStorage.
For second option, it could replace IBackgroundJobClient with MemoryStorage by
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<JobStorage>(x =>
{
return GlobalConfiguration.Configuration.UseMemoryStorage();
});
});
}
}
In conclusion, I suggest you register IBackgroundJobClient and try the second option to achieve your requirement.
Update1
For DB is not available, it could not be resolved by configuring the Dependency Injection. This error is caused by calling services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));.
For resolving this error, you need to overriding this code in Startup.cs.
Try steps below:
Change Startup to below:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//Rest Code
ConfigureHangfire(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//Rest Code
app.UseHangfireServer();
RecurringJob.AddOrUpdate(() => Console.WriteLine("RecurringJob!"), Cron.Minutely);
}
protected virtual void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(config =>
config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"))
);
}
}
Create StartupTest in test project.
public class StartupTest : Startup
{
public StartupTest(IConfiguration configuration) :base(configuration)
{
}
protected override void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(x => x.UseMemoryStorage());
}
}
CustomWebApplicationFactory
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint: class
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder(null)
.UseStartup<TEntryPoint>();
}
}
Test
public class HangfireStorageStartupTest : IClassFixture<CustomWebApplicationFactory<StartupTest>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<StartupTest> _factory;
public HangfireStorageStartupTest(CustomWebApplicationFactory<StartupTest> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
}

Categories