From efbedc60a800dee170dbb2a8bf261e0fedecddfe Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 19 Mar 2026 11:20:44 -0400 Subject: [PATCH] feat(infra): add SessionManager with full session tracking and API key validation --- infra/lmxfakeproxy/LmxFakeProxy.csproj | 6 + infra/lmxfakeproxy/Sessions/SessionManager.cs | 51 ++++++++ .../tests/LmxFakeProxy.Tests/GlobalUsings.cs | 1 + .../LmxFakeProxy.Tests/SessionManagerTests.cs | 116 ++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 infra/lmxfakeproxy/Sessions/SessionManager.cs create mode 100644 infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/GlobalUsings.cs create mode 100644 infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/SessionManagerTests.cs diff --git a/infra/lmxfakeproxy/LmxFakeProxy.csproj b/infra/lmxfakeproxy/LmxFakeProxy.csproj index 1eaf063..59a4378 100644 --- a/infra/lmxfakeproxy/LmxFakeProxy.csproj +++ b/infra/lmxfakeproxy/LmxFakeProxy.csproj @@ -6,6 +6,12 @@ enable + + + + + + diff --git a/infra/lmxfakeproxy/Sessions/SessionManager.cs b/infra/lmxfakeproxy/Sessions/SessionManager.cs new file mode 100644 index 0000000..6a390c7 --- /dev/null +++ b/infra/lmxfakeproxy/Sessions/SessionManager.cs @@ -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 _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; + } +} diff --git a/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/GlobalUsings.cs b/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/SessionManagerTests.cs b/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/SessionManagerTests.cs new file mode 100644 index 0000000..6fe47b9 --- /dev/null +++ b/infra/lmxfakeproxy/tests/LmxFakeProxy.Tests/SessionManagerTests.cs @@ -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("")); + } +}