Initial commit: 健康管家 AI 健康陪伴助手

- Backend: .NET 10 Minimal API + EF Core + PostgreSQL
- Frontend: Flutter + Riverpod + GoRouter + Dio
- AI: DeepSeek LLM + Qwen VLM (OpenAI-compatible)
- Auth: SMS + JWT (access/refresh tokens)
- Features: AI chat, health tracking, medication management, diet analysis, exercise plans, doctor consultations, report analysis
This commit is contained in:
MingNian
2026-06-02 11:11:29 +08:00
commit 14d7c30d3d
144 changed files with 11436 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
using Health.Domain.Entities;
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 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);
}
}