功能测试跳过授权WebApplicationFactory OIDC
Functional test skip authorization WebApplicationFactory OIDC
目标:使用模拟访问令牌创建功能测试但跳过授权(不调用端点)。测试我的网络 API 的控制器方法。 API 通过访问令牌(持有者)受到 authentication/authorization 的保护。与身份服务器 4 通信。
当前:创建了我的自定义 WebApplicationFactory,数据库被播种,访问令牌被创建。
问题:当身份服务器 4 不是 运行 时,测试失败。我不知道如何准确模拟身份服务器。自行创建的访问令牌正在运行。如果身份服务器是 运行,则测试通过授权。
MockJwtToken.cs
public static class MockJwtToken
{
public static string Issuer { get; } = "https://localhost:5001";
public static string Audience { get; } = "https://localhost:5001/resources";
public static SecurityKey SecurityKey { get; }
public static SigningCredentials SigningCredentials { get; }
private static readonly JwtSecurityTokenHandler STokenHandler = new JwtSecurityTokenHandler();
private static readonly RandomNumberGenerator SRng = RandomNumberGenerator.Create();
private static readonly byte[] SKey = new byte[32];
static MockJwtToken()
{
SRng.GetBytes(SKey);
SecurityKey = new SymmetricSecurityKey(SKey) { KeyId = Guid.NewGuid().ToString() };
SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
}
public static string GenerateJwtToken(IEnumerable<Claim> claims)
{
return STokenHandler.WriteToken(new JwtSecurityToken(Issuer, Audience, claims, null, DateTime.UtcNow.AddMinutes(20), SigningCredentials));
}
public static string GenerateJwtTokenAsUser()
{
return GenerateJwtToken(UserClaims);
}
public static List<Claim> UserClaims { get; set; } = new List<Claim>
{
new Claim(JwtClaimTypes.PreferredUserName, "test"),
new Claim(JwtClaimTypes.Email, "test@test.com"),
new Claim(JwtClaimTypes.Subject, "10000000-0000-0000-0000-000000000000"),
new Claim(JwtClaimTypes.Scope, "openid"),
new Claim(JwtClaimTypes.Scope, "api.com:read"),
new Claim(JwtClaimTypes.Scope, "api.com:write"),
};
}
那是我的自定义 WebApplicationFactory
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
private readonly string _connectionString = "DataSource=:memory:";
private readonly SqliteConnection _connection;
public CustomWebApplicationFactory()
{
_connection = new SqliteConnection(_connectionString);
_connection.Open();
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = builder.Build();
var serviceProvider = host.Services;
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<DbContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var dbInit = new DbInitializer(context);
try
{
dbInit.MigrateDatabase();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
try
{
dbInit.SeedAllEnums();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
}
host.Start();
return host;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseSolutionRelativeContentRoot("src/Project.Api.Web")
.ConfigureTestServices(ConfigureServices)
.UseEnvironment("Testing");
}
protected virtual void ConfigureServices(IServiceCollection services)
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJwtToken.Audience;
options.Authority = MockJwtToken.Issuer;
});
// Add ApplicationDbContext using an in-memory database for testing.
services
.AddEntityFrameworkSqlite()
.AddDbContext<DbContext>(options =>
{
options.UseSqlite(_connection);
options.UseInternalServiceProvider(services.BuildServiceProvider());
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_connection.Close();
}
private TokenValidationParameters CreateTokenValidationParameters()
{
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = MockJwtToken.SecurityKey,
SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
JwtSecurityToken jwt = new JwtSecurityToken(token);
return jwt;
},
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireSignedTokens = false,
};
return tokenValidationParameters;
}
}
还使用我的 HttpClient 的扩展来解析响应。
这是我的第一个简单测试 class:
[TestClass]
public class ClientsControllerTest
{
private static CustomWebApplicationFactory<Startup> _factory;
private static HttpClient _client;
private static IServiceScopeFactory _scopeFactory;
private static IServiceScope _scope;
private static DbContext _context;
private static IDbInitializer _dbInit;
[ClassInitialize]
public static void ClassInit(TestContext testContext)
{
Console.WriteLine(testContext.TestName);
_factory = new CustomWebApplicationFactory<Startup>();
_scopeFactory = _factory.Services.GetService<IServiceScopeFactory>();
_scope = _scopeFactory.CreateScope();
_context = _scope.ServiceProvider.GetService<DbContext>();
_dbInit = _scope.ServiceProvider.GetService<IDbInitializer>();
_client = _factory.CreateClient();
}
[TestMethod]
public async Task GetAllAsync_NoData_ReturnsEmptyListWithOk()
{
//arrange
_dbInit.SeedClients();
await _context.SaveChangesAsync();
string at = MockJwtToken.GenerateJwtTokenAsUser();
_client.DefaultRequestHeaders.Add("Authorization", "Bearer " + at);
//act
HttpParsedResponseMessage<ClientModel[]> msg;
ClientModel[] clients;
try
{
msg = await _client.GetAsync<ClientModel[]>("/api/clients");
}
catch (Exception e)
{
throw e;
}
clients = msg.ParsedObject;
//assert
Assert.AreEqual(HttpStatusCode.OK, msg.ResponseMessage.StatusCode);
Assert.AreEqual("application/json; charset=utf-8", msg.ResponseMessage.Content.Headers.ContentType?.ToString());
Assert.AreEqual(1, clients?.Length);
}
[ClassCleanup]
public static void ClassCleanup()
{
_factory.Dispose();
}
}
开始测试时,服务尝试与我的身份服务器通信。我只是想嘲笑这个。这也是堆栈跟踪告诉我的:
!
GetAllAsync_NoData_ReturnsEmptyListWithOk
2021-04-23 10:42:44,977 [4] INFO - Application started. Press Ctrl+C to shut down.
2021-04-23 10:42:45,033 [4] INFO - Hosting environment: Testing
2021-04-23 10:42:45,034 [4] INFO - Content root path: C:\repos\project\Project.Api\src\Project.Api.Web
2021-04-23 10:43:02,901 [8] ERROR - An unhandled exception has occurred while executing the request.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'System.String'.
---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'System.String'.
---> System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it.
---> System.Net.Sockets.SocketException (10061): No connection could be made because the target machine actively refused it.
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts) at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel) --- End of inner exception stack trace --- at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel) at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel) at Microsoft.IdentityModel.Protocols.ConfigurationManager
1.GetConfigurationAsync(CancellationToken cancel)
--- End of inner exception stack trace ---
at Microsoft.IdentityModel.Protocols.ConfigurationManager1.GetConfigurationAsync(CancellationToken cancel) at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync() at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync() at Microsoft.AspNetCore.Authentication.AuthenticationHandler
1.AuthenticateAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)
!
问题是,我还需要覆盖 JwtBearerOptions
中的 Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJsonWebToken.Audience;
options.Authority = MockJsonWebToken.Issuer;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = MockJsonWebToken.Issuer,
};
});
进行此更改后,我可以关闭互联网连接,并且该服务不再尝试与任何发行人通信。
所以这里又是我自己的 CustomWebApplicationFactory。
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
private readonly string _connectionString = "DataSource=:memory:";
private readonly SqliteConnection _connection;
public CustomWebApplicationFactory()
{
_connection = new SqliteConnection(_connectionString);
_connection.Open();
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = builder.Build();
var serviceProvider = host.Services;
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<DbContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var dbInit = new DbInitializer(context);
try
{
dbInit.MigrateDatabase();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
try
{
dbInit.SeedAllEnums();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
}
host.Start();
return host;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseSolutionRelativeContentRoot("src/Project.Api.Web")
.ConfigureTestServices(ConfigureServices)
.UseEnvironment("Testing");
}
protected virtual void ConfigureServices(IServiceCollection services)
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJsonWebToken.Audience;
options.Authority = MockJsonWebToken.Issuer;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = MockJsonWebToken.Issuer,
};
});
services
.AddEntityFrameworkSqlite()
.AddDbContext<DbContext>(options =>
{
options.UseSqlite(_connection);
options.UseInternalServiceProvider(services.BuildServiceProvider());
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_connection.Close();
}
private TokenValidationParameters CreateTokenValidationParameters()
{
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = MockJsonWebToken.SecurityKey,
SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
JwtSecurityToken jwt = new JwtSecurityToken(token);
return jwt;
},
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireSignedTokens = false,
};
return tokenValidationParameters;
}
}
MockJwtToken 没有进一步更改。只是在这里重命名为 MockJsonWebToken 因为 JwtToken 有点多余。
目标:使用模拟访问令牌创建功能测试但跳过授权(不调用端点)。测试我的网络 API 的控制器方法。 API 通过访问令牌(持有者)受到 authentication/authorization 的保护。与身份服务器 4 通信。
当前:创建了我的自定义 WebApplicationFactory,数据库被播种,访问令牌被创建。
问题:当身份服务器 4 不是 运行 时,测试失败。我不知道如何准确模拟身份服务器。自行创建的访问令牌正在运行。如果身份服务器是 运行,则测试通过授权。
MockJwtToken.cs
public static class MockJwtToken
{
public static string Issuer { get; } = "https://localhost:5001";
public static string Audience { get; } = "https://localhost:5001/resources";
public static SecurityKey SecurityKey { get; }
public static SigningCredentials SigningCredentials { get; }
private static readonly JwtSecurityTokenHandler STokenHandler = new JwtSecurityTokenHandler();
private static readonly RandomNumberGenerator SRng = RandomNumberGenerator.Create();
private static readonly byte[] SKey = new byte[32];
static MockJwtToken()
{
SRng.GetBytes(SKey);
SecurityKey = new SymmetricSecurityKey(SKey) { KeyId = Guid.NewGuid().ToString() };
SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
}
public static string GenerateJwtToken(IEnumerable<Claim> claims)
{
return STokenHandler.WriteToken(new JwtSecurityToken(Issuer, Audience, claims, null, DateTime.UtcNow.AddMinutes(20), SigningCredentials));
}
public static string GenerateJwtTokenAsUser()
{
return GenerateJwtToken(UserClaims);
}
public static List<Claim> UserClaims { get; set; } = new List<Claim>
{
new Claim(JwtClaimTypes.PreferredUserName, "test"),
new Claim(JwtClaimTypes.Email, "test@test.com"),
new Claim(JwtClaimTypes.Subject, "10000000-0000-0000-0000-000000000000"),
new Claim(JwtClaimTypes.Scope, "openid"),
new Claim(JwtClaimTypes.Scope, "api.com:read"),
new Claim(JwtClaimTypes.Scope, "api.com:write"),
};
}
那是我的自定义 WebApplicationFactory
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
private readonly string _connectionString = "DataSource=:memory:";
private readonly SqliteConnection _connection;
public CustomWebApplicationFactory()
{
_connection = new SqliteConnection(_connectionString);
_connection.Open();
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = builder.Build();
var serviceProvider = host.Services;
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<DbContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var dbInit = new DbInitializer(context);
try
{
dbInit.MigrateDatabase();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
try
{
dbInit.SeedAllEnums();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
}
host.Start();
return host;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseSolutionRelativeContentRoot("src/Project.Api.Web")
.ConfigureTestServices(ConfigureServices)
.UseEnvironment("Testing");
}
protected virtual void ConfigureServices(IServiceCollection services)
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJwtToken.Audience;
options.Authority = MockJwtToken.Issuer;
});
// Add ApplicationDbContext using an in-memory database for testing.
services
.AddEntityFrameworkSqlite()
.AddDbContext<DbContext>(options =>
{
options.UseSqlite(_connection);
options.UseInternalServiceProvider(services.BuildServiceProvider());
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_connection.Close();
}
private TokenValidationParameters CreateTokenValidationParameters()
{
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = MockJwtToken.SecurityKey,
SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
JwtSecurityToken jwt = new JwtSecurityToken(token);
return jwt;
},
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireSignedTokens = false,
};
return tokenValidationParameters;
}
}
还使用我的 HttpClient 的扩展来解析响应。 这是我的第一个简单测试 class:
[TestClass]
public class ClientsControllerTest
{
private static CustomWebApplicationFactory<Startup> _factory;
private static HttpClient _client;
private static IServiceScopeFactory _scopeFactory;
private static IServiceScope _scope;
private static DbContext _context;
private static IDbInitializer _dbInit;
[ClassInitialize]
public static void ClassInit(TestContext testContext)
{
Console.WriteLine(testContext.TestName);
_factory = new CustomWebApplicationFactory<Startup>();
_scopeFactory = _factory.Services.GetService<IServiceScopeFactory>();
_scope = _scopeFactory.CreateScope();
_context = _scope.ServiceProvider.GetService<DbContext>();
_dbInit = _scope.ServiceProvider.GetService<IDbInitializer>();
_client = _factory.CreateClient();
}
[TestMethod]
public async Task GetAllAsync_NoData_ReturnsEmptyListWithOk()
{
//arrange
_dbInit.SeedClients();
await _context.SaveChangesAsync();
string at = MockJwtToken.GenerateJwtTokenAsUser();
_client.DefaultRequestHeaders.Add("Authorization", "Bearer " + at);
//act
HttpParsedResponseMessage<ClientModel[]> msg;
ClientModel[] clients;
try
{
msg = await _client.GetAsync<ClientModel[]>("/api/clients");
}
catch (Exception e)
{
throw e;
}
clients = msg.ParsedObject;
//assert
Assert.AreEqual(HttpStatusCode.OK, msg.ResponseMessage.StatusCode);
Assert.AreEqual("application/json; charset=utf-8", msg.ResponseMessage.Content.Headers.ContentType?.ToString());
Assert.AreEqual(1, clients?.Length);
}
[ClassCleanup]
public static void ClassCleanup()
{
_factory.Dispose();
}
}
开始测试时,服务尝试与我的身份服务器通信。我只是想嘲笑这个。这也是堆栈跟踪告诉我的:
! GetAllAsync_NoData_ReturnsEmptyListWithOk 2021-04-23 10:42:44,977 [4] INFO - Application started. Press Ctrl+C to shut down. 2021-04-23 10:42:45,033 [4] INFO - Hosting environment: Testing 2021-04-23 10:42:45,034 [4] INFO - Content root path: C:\repos\project\Project.Api\src\Project.Api.Web 2021-04-23 10:43:02,901 [8] ERROR - An unhandled exception has occurred while executing the request. System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'System.String'. ---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'System.String'. ---> System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. ---> System.Net.Sockets.SocketException (10061): No connection could be made because the target machine actively refused it. at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task
1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts) at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel) --- End of inner exception stack trace --- at Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(String address, CancellationToken cancel) at Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(String address, IDocumentRetriever retriever, CancellationToken cancel) at Microsoft.IdentityModel.Protocols.ConfigurationManager
1.GetConfigurationAsync(CancellationToken cancel) --- End of inner exception stack trace --- at Microsoft.IdentityModel.Protocols.ConfigurationManager1.GetConfigurationAsync(CancellationToken cancel) at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync() at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync() at Microsoft.AspNetCore.Authentication.AuthenticationHandler
1.AuthenticateAsync() at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context) !
问题是,我还需要覆盖 JwtBearerOptions
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJsonWebToken.Audience;
options.Authority = MockJsonWebToken.Issuer;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = MockJsonWebToken.Issuer,
};
});
进行此更改后,我可以关闭互联网连接,并且该服务不再尝试与任何发行人通信。
所以这里又是我自己的 CustomWebApplicationFactory。
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
private readonly string _connectionString = "DataSource=:memory:";
private readonly SqliteConnection _connection;
public CustomWebApplicationFactory()
{
_connection = new SqliteConnection(_connectionString);
_connection.Open();
}
protected override IHost CreateHost(IHostBuilder builder)
{
var host = builder.Build();
var serviceProvider = host.Services;
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<DbContext>();
var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
var dbInit = new DbInitializer(context);
try
{
dbInit.MigrateDatabase();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
try
{
dbInit.SeedAllEnums();
}
catch (Exception e)
{
logger.LogError(e, "An error occurred seeding the " + $"database with test messages. Error: {e.Message}");
}
}
host.Start();
return host;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder
.UseSolutionRelativeContentRoot("src/Project.Api.Web")
.ConfigureTestServices(ConfigureServices)
.UseEnvironment("Testing");
}
protected virtual void ConfigureServices(IServiceCollection services)
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = CreateTokenValidationParameters();
options.Audience = MockJsonWebToken.Audience;
options.Authority = MockJsonWebToken.Issuer;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = MockJsonWebToken.Issuer,
};
});
services
.AddEntityFrameworkSqlite()
.AddDbContext<DbContext>(options =>
{
options.UseSqlite(_connection);
options.UseInternalServiceProvider(services.BuildServiceProvider());
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_connection.Close();
}
private TokenValidationParameters CreateTokenValidationParameters()
{
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = MockJsonWebToken.SecurityKey,
SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
JwtSecurityToken jwt = new JwtSecurityToken(token);
return jwt;
},
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireSignedTokens = false,
};
return tokenValidationParameters;
}
}
MockJwtToken 没有进一步更改。只是在这里重命名为 MockJsonWebToken 因为 JwtToken 有点多余。