using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Jiaowu.Api.Tests; public sealed class AuthSessionServiceTests { [Fact] public async Task Web_session_rotates_refresh_token_and_rejects_reuse() { await using var fixture = await SessionFixture.CreateAsync(); var issued = await fixture.Service.CreateAsync( fixture.User, [SystemRoles.Student], AuthenticationClientType.Web); Assert.InRange( issued.SessionExpiresAt, DateTime.UtcNow.AddMinutes(29), DateTime.UtcNow.AddMinutes(31)); var stored = await fixture.Db.RefreshSessions.SingleAsync(); Assert.NotEqual(issued.RefreshToken, stored.TokenHash); var refreshed = await fixture.Service.RefreshAsync(issued.RefreshToken); Assert.NotNull(refreshed); Assert.NotEqual(issued.RefreshToken, refreshed.RefreshToken); Assert.Null(await fixture.Service.RefreshAsync(issued.RefreshToken)); Assert.Equal(1, await fixture.Db.RefreshSessions.CountAsync(x => x.RevokedAt == null)); } [Fact] public async Task App_session_uses_three_day_sliding_window() { await using var fixture = await SessionFixture.CreateAsync(); var issued = await fixture.Service.CreateAsync( fixture.User, [SystemRoles.Student], AuthenticationClientType.App); Assert.InRange( issued.SessionExpiresAt, DateTime.UtcNow.AddDays(3).AddMinutes(-1), DateTime.UtcNow.AddDays(3).AddMinutes(1)); } private sealed class SessionFixture : IAsyncDisposable { private readonly SqliteConnection _connection; private readonly ServiceProvider _provider; private readonly AsyncServiceScope _scope; private SessionFixture( SqliteConnection connection, ServiceProvider provider, AsyncServiceScope scope, AppDbContext db, ApplicationUser user, IAuthSessionService service) { _connection = connection; _provider = provider; _scope = scope; Db = db; User = user; Service = service; } public AppDbContext Db { get; } public ApplicationUser User { get; } public IAuthSessionService Service { get; } public static async Task CreateAsync() { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var services = new ServiceCollection(); services.AddLogging(); services.AddDbContext(options => options.UseSqlite(connection)); services.Configure(options => { options.Issuer = "tests"; options.Audience = "tests-web"; options.Key = "a-test-signing-key-that-is-at-least-32-bytes-long"; options.AccessTokenMinutes = 10; options.WebIdleMinutes = 30; options.AppIdleMinutes = 4320; }); services .AddIdentityCore() .AddRoles() .AddEntityFrameworkStores(); services.AddScoped(); services.AddScoped(); var provider = services.BuildServiceProvider(); var scope = provider.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(); var userManager = scope.ServiceProvider.GetRequiredService>(); var user = new ApplicationUser { UserName = "session-user", DisplayName = "会话测试用户", IsEnabled = true, LockoutEnabled = true }; Assert.True((await userManager.CreateAsync(user, "SessionUser@123")).Succeeded); return new SessionFixture( connection, provider, scope, db, user, scope.ServiceProvider.GetRequiredService()); } public async ValueTask DisposeAsync() { await _scope.DisposeAsync(); await _provider.DisposeAsync(); await _connection.DisposeAsync(); } } }