You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为依赖外部身份提供者的授权ASP.NET Web API编写集成测试?

针对OpenIddict自省验证场景的集成测试方案

1. 搭建测试基础环境

在集成测试项目中,使用WebApplicationFactory<T>启动API项目,并通过ConfigureServices替换或模拟依赖服务,避免实际调用身份服务器:

public class ApiTestFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 移除原有OpenIddict验证服务,替换为模拟实现
            var validationServiceDescriptor = services.SingleOrDefault(d => 
                d.ServiceType == typeof(IOpenIddictValidationService));
            if (validationServiceDescriptor != null)
            {
                services.Remove(validationServiceDescriptor);
            }

            // 模拟自省验证逻辑:根据令牌前缀判断是否有效
            services.AddSingleton<IOpenIddictValidationService>(sp =>
            {
                var mock = new Mock<IOpenIddictValidationService>();
                mock.Setup(s => s.IntrospectTokenAsync(
                    It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
                    .ReturnsAsync((string token, string _, CancellationToken _) =>
                    {
                        if (token.StartsWith("test-valid-token"))
                        {
                            return new OpenIddictValidationIntrospectionResult
                            {
                                IsActive = true,
                                Claims = new Dictionary<string, object>
                                {
                                    { "sub", "test-user-001" },
                                    { "client_id", "test-web-client" }
                                }
                            };
                        }
                        return new OpenIddictValidationIntrospectionResult { IsActive = false };
                    });
                return mock.Object;
            });

            // 模拟获取额外声明的服务(替换原调用身份服务器的逻辑)
            var claimServiceDescriptor = services.SingleOrDefault(d => 
                d.ServiceType == typeof(IAdditionalClaimProvider));
            if (claimServiceDescriptor != null)
            {
                services.Remove(claimServiceDescriptor);
                services.AddSingleton<IAdditionalClaimProvider>(sp =>
                {
                    var mock = new Mock<IAdditionalClaimProvider>();
                    mock.Setup(s => s.GetUserClaimsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
                        .ReturnsAsync((string userId, CancellationToken _) =>
                        {
                            return new Dictionary<string, object>
                            {
                                { "role", userId == "test-user-001" ? "admin" : "user" },
                                { "permission", "view-data" }
                            };
                        });
                    return mock.Object;
                });
            }
        });
    }
}

2. 可选:通过HTTP拦截模拟身份服务器端点

如果不想直接替换服务,也可以用DelegatingHandler拦截API向身份服务器发起的请求,返回预设响应:

public class MockIdentityServerHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 模拟自省端点
        if (request.RequestUri?.AbsolutePath == "/connect/introspect")
        {
            var formData = await request.Content.ReadFromFormAsync(cancellationToken);
            var token = formData["token"];
            return token == "test-valid-token" 
                ? new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = JsonContent.Create(new { active = true, sub = "test-user-001", client_id = "test-web-client" })
                }
                : new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = JsonContent.Create(new { active = false })
                };
        }

        // 模拟额外声明获取端点
        if (request.RequestUri?.AbsolutePath == "/api/user/claims")
        {
            var userId = request.Headers.GetValues("X-User-Id").FirstOrDefault();
            return userId == "test-user-001"
                ? new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = JsonContent.Create(new { role = "admin", permission = "view-data" })
                }
                : new HttpResponseMessage(HttpStatusCode.NotFound);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

在测试工厂中注册该拦截器:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.ConfigureServices(services =>
    {
        services.AddHttpClient("IdentityServerClient")
            .AddHttpMessageHandler<MockIdentityServerHandler>();
        services.AddSingleton<MockIdentityServerHandler>();
    });
}

3. 编写测试用例

基于测试工厂,覆盖认证、授权的核心场景:

3.1 有效令牌访问受保护接口

[Fact]
public async Task Get_ProtectedApi_WithValidToken_ReturnsOk()
{
    // Arrange
    var factory = new ApiTestFactory();
    var client = factory.CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Bearer", "test-valid-token");

    // Act
    var response = await client.GetAsync("/api/data/protected");

    // Assert
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    Assert.Contains("test-user-001", content);
}

3.2 无效令牌被拒绝访问

[Fact]
public async Task Get_ProtectedApi_WithInvalidToken_ReturnsUnauthorized()
{
    // Arrange
    var factory = new ApiTestFactory();
    var client = factory.CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Bearer", "test-invalid-token");

    // Act
    var response = await client.GetAsync("/api/data/protected");

    // Assert
    Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

3.3 验证角色权限控制

[Fact]
public async Task Get_AdminApi_WithAdminClaim_ReturnsOk()
{
    // Arrange
    var factory = new ApiTestFactory();
    var client = factory.CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Bearer", "test-valid-token");

    // Act
    var response = await client.GetAsync("/api/admin/manage");

    // Assert
    response.EnsureSuccessStatusCode();
}

[Fact]
public async Task Get_AdminApi_WithoutAdminClaim_ReturnsForbidden()
{
    // Arrange
    var factory = new ApiTestFactory();
    var client = factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureServices(services =>
        {
            // 替换模拟服务,返回普通用户角色
            var claimServiceDescriptor = services.SingleOrDefault(d => 
                d.ServiceType == typeof(IAdditionalClaimProvider));
            if (claimServiceDescriptor != null)
            {
                services.Remove(claimServiceDescriptor);
                services.AddSingleton<IAdditionalClaimProvider>(sp =>
                {
                    var mock = new Mock<IAdditionalClaimProvider>();
                    mock.Setup(s => s.GetUserClaimsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
                        .ReturnsAsync(new Dictionary<string, object> { { "role", "user" } });
                    return mock.Object;
                });
            }
        });
    }).CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Bearer", "test-valid-token");

    // Act
    var response = await client.GetAsync("/api/admin/manage");

    // Assert
    Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}

关键注意事项

  • 模拟逻辑要完全覆盖业务中的认证授权规则,比如令牌有效性判断、声明内容、角色权限映射等。
  • 若使用OpenIddictValidationEvents事件处理程序,需确保模拟逻辑能触发对应事件,或直接替换事件委托。
  • 补充边界场景测试:如令牌过期、客户端ID不匹配、缺失必要声明等。

内容的提问来源于stack exchange,提问作者dmdany07

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 05:17:26