feat(infra): add SessionManager with full session tracking and API key validation

This commit is contained in:
Joseph Doherty
2026-03-19 11:20:44 -04:00
parent 1d498b94b4
commit efbedc60a8
4 changed files with 174 additions and 0 deletions

View File

@@ -6,6 +6,12 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="tests\**\*" />
<Content Remove="tests\**\*" />
<None Remove="tests\**\*" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="Protos/scada.proto" GrpcServices="Server" /> <Protobuf Include="Protos/scada.proto" GrpcServices="Server" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
namespace LmxFakeProxy.Sessions;
public record SessionInfo(string ClientId, long ConnectedSinceUtcTicks);
public class SessionManager
{
private readonly string? _requiredApiKey;
private readonly ConcurrentDictionary<string, SessionInfo> _sessions = new();
public SessionManager(string? requiredApiKey)
{
_requiredApiKey = requiredApiKey;
}
public (bool Success, string Message, string SessionId) Connect(string clientId, string apiKey)
{
if (!CheckApiKey(apiKey))
return (false, "Invalid API key", string.Empty);
var sessionId = Guid.NewGuid().ToString("N");
var info = new SessionInfo(clientId, DateTime.UtcNow.Ticks);
_sessions[sessionId] = info;
return (true, "Connected", sessionId);
}
public bool Disconnect(string sessionId)
{
return _sessions.TryRemove(sessionId, out _);
}
public bool ValidateSession(string sessionId)
{
return _sessions.ContainsKey(sessionId);
}
public (bool Found, string ClientId, long ConnectedSinceUtcTicks) GetConnectionState(string sessionId)
{
if (_sessions.TryGetValue(sessionId, out var info))
return (true, info.ClientId, info.ConnectedSinceUtcTicks);
return (false, string.Empty, 0);
}
public bool CheckApiKey(string apiKey)
{
if (string.IsNullOrEmpty(_requiredApiKey))
return true;
return apiKey == _requiredApiKey;
}
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -0,0 +1,116 @@
namespace LmxFakeProxy.Tests;
using LmxFakeProxy.Sessions;
public class SessionManagerTests
{
[Fact]
public void Connect_ReturnsUniqueSessionId()
{
var mgr = new SessionManager(null);
var (ok1, _, id1) = mgr.Connect("client1", "");
var (ok2, _, id2) = mgr.Connect("client2", "");
Assert.True(ok1);
Assert.True(ok2);
Assert.NotEqual(id1, id2);
}
[Fact]
public void Connect_WithValidApiKey_Succeeds()
{
var mgr = new SessionManager("secret");
var (ok, _, _) = mgr.Connect("client1", "secret");
Assert.True(ok);
}
[Fact]
public void Connect_WithInvalidApiKey_Fails()
{
var mgr = new SessionManager("secret");
var (ok, msg, id) = mgr.Connect("client1", "wrong");
Assert.False(ok);
Assert.Empty(id);
Assert.Contains("Invalid API key", msg);
}
[Fact]
public void Connect_WithNoKeyConfigured_AcceptsAnyKey()
{
var mgr = new SessionManager(null);
var (ok1, _, _) = mgr.Connect("c1", "anykey");
var (ok2, _, _) = mgr.Connect("c2", "");
Assert.True(ok1);
Assert.True(ok2);
}
[Fact]
public void Disconnect_RemovesSession()
{
var mgr = new SessionManager(null);
var (_, _, id) = mgr.Connect("client1", "");
Assert.True(mgr.ValidateSession(id));
var ok = mgr.Disconnect(id);
Assert.True(ok);
Assert.False(mgr.ValidateSession(id));
}
[Fact]
public void Disconnect_UnknownSession_ReturnsFalse()
{
var mgr = new SessionManager(null);
Assert.False(mgr.Disconnect("nonexistent"));
}
[Fact]
public void ValidateSession_ValidId_ReturnsTrue()
{
var mgr = new SessionManager(null);
var (_, _, id) = mgr.Connect("client1", "");
Assert.True(mgr.ValidateSession(id));
}
[Fact]
public void ValidateSession_InvalidId_ReturnsFalse()
{
var mgr = new SessionManager(null);
Assert.False(mgr.ValidateSession("bogus"));
}
[Fact]
public void GetConnectionState_ReturnsCorrectInfo()
{
var mgr = new SessionManager(null);
var (_, _, id) = mgr.Connect("myClient", "");
var (found, clientId, ticks) = mgr.GetConnectionState(id);
Assert.True(found);
Assert.Equal("myClient", clientId);
Assert.True(ticks > 0);
}
[Fact]
public void GetConnectionState_UnknownSession_ReturnsNotConnected()
{
var mgr = new SessionManager(null);
var (found, clientId, ticks) = mgr.GetConnectionState("unknown");
Assert.False(found);
Assert.Empty(clientId);
Assert.Equal(0, ticks);
}
[Fact]
public void CheckApiKey_NoKeyConfigured_AlwaysValid()
{
var mgr = new SessionManager(null);
Assert.True(mgr.CheckApiKey("anything"));
Assert.True(mgr.CheckApiKey(""));
}
[Fact]
public void CheckApiKey_WithKeyConfigured_ValidatesCorrectly()
{
var mgr = new SessionManager("mykey");
Assert.True(mgr.CheckApiKey("mykey"));
Assert.False(mgr.CheckApiKey("wrong"));
Assert.False(mgr.CheckApiKey(""));
}
}