3213d4c718
- 重构架构:抽离AddCloudSyncServer到扩展类,补全中间层文档对齐 - 修复根路径404与鉴权跳转:新增中间件处理浏览器请求302跳转 - 实现种子数据初始化:添加默认管理员账号自动创建逻辑 - 优化前端代理与构建:新增Vite代理配置与自动构建Target - 补全测试与文档:新增集成测试,更新开发与部署文档
139 lines
4.8 KiB
C#
139 lines
4.8 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using Hua.Todo.Application.Common;
|
|
using Hua.Todo.Application.Repositories;
|
|
using Hua.Todo.Application.Services.CloudSync.Models;
|
|
using Hua.Todo.Core.Entities;
|
|
using Hua.Todo.HttpApi.AspNetCore.CloudSync;
|
|
using Hua.Todo.HttpApi.AspNetCore.DynamicApi;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Xunit;
|
|
|
|
namespace Hua.Todo.Host.Tests.CloudSync;
|
|
|
|
/// <summary>
|
|
/// CloudSync 登录后访问安全策略的端到端测试。
|
|
/// 覆盖 DynamicApi 响应包装、SessionAuthenticationHandler 认证和 SecurityPolicyService 当前用户解析链路。
|
|
/// </summary>
|
|
public sealed class CloudSyncAuthSecurityPolicyTests : IDisposable
|
|
{
|
|
private readonly SqliteConnection _connection;
|
|
private readonly WebApplication _app;
|
|
private readonly HttpClient _client;
|
|
|
|
/// <summary>
|
|
/// 创建使用 SQLite 内存库的最小 Host 管道。
|
|
/// </summary>
|
|
public CloudSyncAuthSecurityPolicyTests()
|
|
{
|
|
_connection = new SqliteConnection("Data Source=:memory:");
|
|
_connection.Open();
|
|
|
|
var builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseUrls("http://127.0.0.1:0");
|
|
builder.Services.AddApplicationServices("Data Source=:memory:");
|
|
builder.Services.AddDbContext<TodoDbContext>(options => options.UseSqlite(_connection));
|
|
builder.Services.AddCloudSyncServer();
|
|
|
|
_app = builder.Build();
|
|
_app.UseAuthentication();
|
|
_app.UseAuthorization();
|
|
_app.UseDynamicApi();
|
|
|
|
using (var scope = _app.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
|
|
db.Database.EnsureCreated();
|
|
SeedAdmin(db, scope.ServiceProvider.GetRequiredService<IPasswordHasher<UserEntity>>());
|
|
}
|
|
|
|
_app.StartAsync().GetAwaiter().GetResult();
|
|
_client = new HttpClient { BaseAddress = new Uri(_app.Urls.First()) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证登录成功后,携带返回的 Bearer Token 能获取当前用户安全策略。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task GetSecurityPolicy_AfterLogin_ReturnsPolicy()
|
|
{
|
|
var loginResponse = await _client.PostAsJsonAsync("/api/auth/login", new LoginRequest
|
|
{
|
|
UserName = "admin",
|
|
Password = "123456"
|
|
});
|
|
|
|
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
|
|
var loginPayload = await ReadDynamicApiResponseAsync<LoginResponse>(loginResponse);
|
|
Assert.True(loginPayload.Success);
|
|
Assert.False(string.IsNullOrWhiteSpace(loginPayload.Data?.AccessToken));
|
|
|
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginPayload.Data.AccessToken);
|
|
var policyResponse = await _client.GetAsync("/api/security/policy");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, policyResponse.StatusCode);
|
|
var policyPayload = await ReadDynamicApiResponseAsync<SecurityPolicyDto>(policyResponse);
|
|
Assert.True(policyPayload.Success);
|
|
Assert.True(policyPayload.Data?.AllowPersist);
|
|
Assert.True(policyPayload.Data?.AllowSync);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 释放测试 Host 与 SQLite 连接。
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_client.Dispose();
|
|
_app.StopAsync().GetAwaiter().GetResult();
|
|
_app.DisposeAsync().GetAwaiter().GetResult();
|
|
_connection.Dispose();
|
|
}
|
|
|
|
private static void SeedAdmin(TodoDbContext db, IPasswordHasher<UserEntity> hasher)
|
|
{
|
|
var user = new UserEntity
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserName = "admin",
|
|
Role = "admin",
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
UpdatedAtUtc = DateTime.UtcNow
|
|
};
|
|
user.PasswordHash = hasher.HashPassword(user, "123456");
|
|
|
|
db.Users.Add(user);
|
|
db.SecurityPolicies.Add(new SecurityPolicyEntity
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserId = user.Id,
|
|
AllowPersist = true,
|
|
AllowSync = true,
|
|
IsTrustedDeviceOnly = false
|
|
});
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private static async Task<DynamicApiPayload<T>> ReadDynamicApiResponseAsync<T>(HttpResponseMessage response)
|
|
{
|
|
var payload = await response.Content.ReadFromJsonAsync<DynamicApiPayload<T>>(new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
});
|
|
|
|
return payload ?? throw new InvalidOperationException("DynamicApi 响应为空。");
|
|
}
|
|
|
|
private sealed class DynamicApiPayload<T>
|
|
{
|
|
public bool Success { get; set; }
|
|
public T? Data { get; set; }
|
|
}
|
|
}
|