## 后端安全加固 - 新增 UserUploadPathResolver: 用户上传文件路径安全解析, 防目录穿越 - LocalReportFileStorage: 文件存储路径安全加固 - local_account_file_cleanup: 账号删除时文件清理逻辑增强 - AuthService: 认证逻辑增强 - file_endpoints / report_endpoints: 文件访问接口安全加固 - ai_chat_endpoints / doctor_endpoints: 接口安全调整 - Program.cs: 服务注册调整 ## 前端认证与媒体 - 新增 authenticated_network_image.dart: 带认证的图片加载组件 - auth_provider: 认证状态管理大幅增强(+173) - api_client: 网络客户端增强(+124) - chat_provider: 聊天 provider 重构(+76) - omron_device_provider: 蓝牙设备 provider 增强(+53) - sse_handler: SSE 处理增强(+35) - consultation_provider / data_providers / conversation_history_provider: 调整 ## 页面调整 - remaining_pages: 健康档案/饮食记录等页面增强(+115) - home_page / chat_messages_view: 主页微调 - doctor 端多页微调(consultations/dashboard/followups/patient_detail/profile/report_detail/reports) - report_pages / settings_pages / notification_prefs_page: 微调 - device_scan_page / diet_capture_page / admin_home_page: 微调 ## 测试 - 新增 file_path_security_tests: 文件路径安全测试 - 新增 protected_media_url_test: 媒体URL保护测试 - 新增 user_session_identity_test: 用户会话身份测试 - account_deletion_tests / application_service_tests / auth_tests: 更新
187 lines
6.4 KiB
C#
187 lines
6.4 KiB
C#
using System.Text.Json;
|
||
using Health.Domain.Entities;
|
||
using Health.Infrastructure.Auth;
|
||
using Health.Infrastructure.Data;
|
||
using Health.Infrastructure.Services;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Configuration;
|
||
|
||
namespace Health.Tests;
|
||
|
||
/// <summary>
|
||
/// 认证流程测试
|
||
/// </summary>
|
||
public class AuthTests
|
||
{
|
||
private AppDbContext CreateDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||
.Options;
|
||
return new AppDbContext(options);
|
||
}
|
||
|
||
private IConfiguration CreateConfig()
|
||
{
|
||
var settings = new Dictionary<string, string?>
|
||
{
|
||
{ "JWT_SECRET", "test-secret-key-for-unit-tests-min-32-chars!!" },
|
||
{ "JWT_ISSUER", "health-manager" },
|
||
{ "JWT_AUDIENCE", "health-manager-app" }
|
||
};
|
||
return new ConfigurationBuilder().AddInMemoryCollection(settings).Build();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task SendSms_Should_Create_VerificationCode()
|
||
{
|
||
// Arrange
|
||
using var db = CreateDbContext();
|
||
var sms = new SmsService();
|
||
|
||
// Act
|
||
var code = sms.GenerateCode();
|
||
db.VerificationCodes.Add(new VerificationCode
|
||
{
|
||
Id = Guid.NewGuid(), Phone = "13800138000", Code = code,
|
||
ExpiresAt = DateTime.UtcNow.AddMinutes(5),
|
||
});
|
||
await db.SaveChangesAsync();
|
||
|
||
// Assert
|
||
var saved = await db.VerificationCodes.FirstOrDefaultAsync(v => v.Phone == "13800138000");
|
||
Assert.NotNull(saved);
|
||
Assert.Equal(code, saved!.Code);
|
||
Assert.True(saved.ExpiresAt > DateTime.UtcNow);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Login_Should_Create_User_And_Return_Token()
|
||
{
|
||
// Arrange
|
||
using var db = CreateDbContext();
|
||
var config = CreateConfig();
|
||
var jwt = new JwtProvider(config);
|
||
|
||
var phone = "13800138000";
|
||
|
||
// Act
|
||
var user = new User { Id = Guid.NewGuid(), Phone = phone, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
|
||
db.Users.Add(user);
|
||
await db.SaveChangesAsync();
|
||
|
||
var accessToken = jwt.GenerateAccessToken(user.Id, phone);
|
||
var refreshToken = jwt.GenerateRefreshToken();
|
||
db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = user.Id, Token = refreshToken, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||
await db.SaveChangesAsync();
|
||
|
||
// Assert
|
||
Assert.NotNull(accessToken);
|
||
Assert.True(accessToken.Length > 50);
|
||
Assert.NotNull(refreshToken);
|
||
Assert.True(refreshToken.Length > 50);
|
||
|
||
var savedUser = await db.Users.FirstOrDefaultAsync(u => u.Phone == phone);
|
||
Assert.NotNull(savedUser);
|
||
|
||
var savedToken = await db.RefreshTokens.FirstOrDefaultAsync(t => t.Token == refreshToken);
|
||
Assert.NotNull(savedToken);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task RefreshToken_Should_Revoke_Old_And_Issue_New()
|
||
{
|
||
// Arrange
|
||
using var db = CreateDbContext();
|
||
var config = CreateConfig();
|
||
var jwt = new JwtProvider(config);
|
||
|
||
var user = new User { Id = Guid.NewGuid(), Phone = "13800138000", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
|
||
db.Users.Add(user);
|
||
|
||
var oldRefresh = jwt.GenerateRefreshToken();
|
||
db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = user.Id, Token = oldRefresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||
await db.SaveChangesAsync();
|
||
|
||
// Act — 吊销旧 token,签发新 token
|
||
var old = await db.RefreshTokens.FirstOrDefaultAsync(t => t.Token == oldRefresh && !t.IsRevoked);
|
||
Assert.NotNull(old);
|
||
old!.IsRevoked = true;
|
||
|
||
var newToken = jwt.GenerateRefreshToken();
|
||
db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = user.Id, Token = newToken, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||
await db.SaveChangesAsync();
|
||
|
||
// Assert
|
||
var revoked = await db.RefreshTokens.FirstOrDefaultAsync(t => t.Token == oldRefresh);
|
||
Assert.True(revoked!.IsRevoked);
|
||
|
||
var active = await db.RefreshTokens.FirstOrDefaultAsync(t => t.Token == newToken && !t.IsRevoked);
|
||
Assert.NotNull(active);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Refresh_Should_Return_Stable_User_Identity()
|
||
{
|
||
using var db = CreateDbContext();
|
||
var config = CreateConfig();
|
||
var jwt = new JwtProvider(config);
|
||
var user = new User
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Phone = "13800138000",
|
||
Role = "User",
|
||
Name = "测试用户",
|
||
CreatedAt = DateTime.UtcNow,
|
||
UpdatedAt = DateTime.UtcNow
|
||
};
|
||
var oldRefresh = jwt.GenerateRefreshToken();
|
||
db.Users.Add(user);
|
||
db.RefreshTokens.Add(new RefreshToken
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
UserId = user.Id,
|
||
Token = oldRefresh,
|
||
ExpiresAt = DateTime.UtcNow.AddDays(30)
|
||
});
|
||
await db.SaveChangesAsync();
|
||
var service = new AuthService(
|
||
db,
|
||
jwt,
|
||
new SmsService(),
|
||
new AppleTokenValidator(config));
|
||
|
||
var result = await service.RefreshAsync(oldRefresh, CancellationToken.None);
|
||
|
||
Assert.Equal(0, result.Code);
|
||
var data = JsonSerializer.SerializeToElement(result.Data);
|
||
var refreshedUser = data.GetProperty("user");
|
||
Assert.Equal(user.Id, refreshedUser.GetProperty("Id").GetGuid());
|
||
Assert.Equal(user.Phone, refreshedUser.GetProperty("Phone").GetString());
|
||
Assert.Equal(user.Role, refreshedUser.GetProperty("Role").GetString());
|
||
}
|
||
|
||
[Fact]
|
||
public async Task VerificationCode_Expired_Should_Fail_Login()
|
||
{
|
||
// Arrange
|
||
using var db = CreateDbContext();
|
||
var expiredCode = new VerificationCode
|
||
{
|
||
Id = Guid.NewGuid(), Phone = "13800138000", Code = "123456",
|
||
ExpiresAt = DateTime.UtcNow.AddMinutes(-1), // 已过期
|
||
};
|
||
db.VerificationCodes.Add(expiredCode);
|
||
await db.SaveChangesAsync();
|
||
|
||
// Act
|
||
var valid = await db.VerificationCodes
|
||
.Where(v => v.Phone == "13800138000" && v.Code == "123456"
|
||
&& v.ExpiresAt > DateTime.UtcNow && !v.IsUsed)
|
||
.FirstOrDefaultAsync();
|
||
|
||
// Assert — 过期的验证码查不到
|
||
Assert.Null(valid);
|
||
}
|
||
}
|