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(""));
+ }
+}