Files
StoreWeb/Tests/Store.Security.Tests/PersistentSessionTests.cs
T
2026-10-23 19:28:17 +08:00

78 lines
3.6 KiB
C#

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<OnlineUserInfo>();
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<OnlineUserInfo>().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<OnlineUserInfo>();
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<OnlineUserInfo>().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<OnlineUserInfo>().Count());
}
private static IFreeSql Open(string path) => new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Sqlite, $"Data Source={path};Pooling=false")
.Build();
}