using System.Security.Claims; using MAX.Framework.Security; using Microsoft.Extensions.Options; namespace Store.Users.Tests; public sealed class PersistentSessionTests { [Fact] public async Task TokensSurviveRestart_AndLogoutOnlyRevokesCurrentSession() { var path = Path.Combine(Path.GetTempPath(), $"sessions-{Guid.NewGuid():N}.db"); var options = Options.Create(new JwtOptions()); TokenResult first; TokenResult second; string firstJti; try { using (var db = Open(path)) { db.CodeFirst.SyncStructure(); var store = new OnlineUserStore(db); var tokens = new TokenService(options, store); first = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "same")], "192.0.2.10"); second = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "same")], "2001:db8::1"); firstJti = (await tokens.ValidateTokenAsync(first.Token))!.FindFirst(JwtClaimTypes.Jti)!.Value; Assert.NotNull(await tokens.ValidateTokenAsync(second.Token)); Assert.Equal(2, store.Count); } // 新 ORM、新存储和 TokenService,模拟进程重启后的数据库恢复。 using (var db = Open(path)) { var store = new OnlineUserStore(db); var tokens = new TokenService(options, store); Assert.NotNull(await tokens.ValidateTokenAsync(first.Token)); Assert.NotNull(await tokens.ValidateTokenAsync(second.Token)); Assert.Contains(store.GetOnlineUsers(), x => x.LoginIp == "2001:db8::1"); store.SignOutSession("other", firstJti); Assert.NotNull(await tokens.ValidateTokenAsync(first.Token)); store.SignOutSession("same", firstJti); Assert.Null(await tokens.ValidateTokenAsync(first.Token)); Assert.NotNull(await tokens.ValidateTokenAsync(second.Token)); Assert.Single(store.GetOnlineUsers()); Assert.NotNull(db.Select().Where(x => x.Jti == firstJti).First().RevokedAt); } using (var db = Open(path)) { var store = new OnlineUserStore(db); var tokens = new TokenService(options, store); Assert.Null(await tokens.ValidateTokenAsync(first.Token)); Assert.NotNull(await tokens.ValidateTokenAsync(second.Token)); store.SignOut("same"); Assert.Null(await tokens.ValidateTokenAsync(second.Token)); } } finally { File.Delete(path); } } [Fact] public async Task ExpiredSessionsAreRejected_EvenWithinJwtClockTolerance() { using var db = Open(":memory:"); db.CodeFirst.SyncStructure(); var store = new OnlineUserStore(db); var tokens = new TokenService(Options.Create(new JwtOptions()), store); var token = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "user")]); db.Update().Set(x => x.ExpiresAt, DateTime.UtcNow.AddSeconds(-1)) .Where(x => x.UserName == "user").ExecuteAffrows(); Assert.Null(await tokens.ValidateTokenAsync(token.Token)); Assert.Empty(store.GetOnlineUsers()); Assert.Equal(1, db.Select().Count()); } private static IFreeSql Open(string path) => new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.Sqlite, $"Data Source={path};Pooling=false") .Build(); }