Files
esiur-dotnet/Tests/Unit/Integration/SessionHeadersIntegrationTests.cs
T
2026-08-21 11:17:55 +03:00

400 lines
16 KiB
C#

namespace Esiur.Tests.Unit.Integration;
using Esiur.Data;
using Esiur.Net.Sockets;
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Security.Authority.Providers;
using Esiur.Security.Cryptography;
using Esiur.Stores;
using System.Net;
[Collection("Integration")]
public class SessionHeadersIntegrationTests
{
[Fact]
public async Task Handshake_ExchangesParserAndEncryptedRecordBudgetsInBothDirections()
{
await using var cluster = await IntegrationCluster
.StartAsync(
warehouse =>
{
warehouse.Configuration.Parser.MaximumPacketSize = 7_100_001;
warehouse.Configuration.Parser.MaximumAllocationSize = 3_100_002;
warehouse.Configuration.Parser.MaximumCollectionItems = 51_003;
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 54;
warehouse.Configuration.Encryption.MaximumRecordSize = 7_101_004;
return Task.CompletedTask;
},
populateClient: warehouse =>
{
warehouse.Configuration.Parser.MaximumPacketSize = 6_200_001;
warehouse.Configuration.Parser.MaximumAllocationSize = 2_200_002;
warehouse.Configuration.Parser.MaximumCollectionItems = 42_003;
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 45;
warehouse.Configuration.Encryption.MaximumRecordSize = 6_201_004;
return Task.CompletedTask;
})
.WaitAsync(TimeSpan.FromSeconds(10));
var serverConnection = Assert.Single(cluster.Server.Connections);
Assert.Equal(7_100_001u, cluster.Connection.Session.RemoteMaximumPacketSize);
Assert.Equal(3_100_002u, cluster.Connection.Session.RemoteMaximumAllocationSize);
Assert.Equal(51_003, cluster.Connection.Session.RemoteMaximumCollectionItems);
Assert.Equal(54, cluster.Connection.Session.RemoteMaximumTypeMetadataDepth);
Assert.Equal(7_101_004u, cluster.Connection.Session.RemoteMaximumEncryptedRecordSize);
Assert.Equal(6_200_001u, serverConnection.Session.RemoteMaximumPacketSize);
Assert.Equal(2_200_002u, serverConnection.Session.RemoteMaximumAllocationSize);
Assert.Equal(42_003, serverConnection.Session.RemoteMaximumCollectionItems);
Assert.Equal(45, serverConnection.Session.RemoteMaximumTypeMetadataDepth);
Assert.Equal(6_201_004u, serverConnection.Session.RemoteMaximumEncryptedRecordSize);
}
[Fact]
public async Task AcceptedConnection_RaisesReadyAndDisconnectedLifecycleEvents()
{
var ready = new TaskCompletionSource<EpConnection>(
TaskCreationOptions.RunContinuationsAsynchronously);
var disconnected = new TaskCompletionSource<EpConnection>(
TaskCreationOptions.RunContinuationsAsynchronously);
await using var cluster = await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
serverCreated: server =>
{
server.ConnectionReady += connection => ready.TrySetResult(connection);
server.ConnectionDisconnected += connection => disconnected.TrySetResult(connection);
})
.WaitAsync(TimeSpan.FromSeconds(10));
var accepted = await ready.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(accepted.Session.Authenticated);
Assert.Equal("tester", accepted.Session.RemoteIdentity);
accepted.NetworkClose(accepted.Socket);
Assert.Same(
accepted,
await disconnected.Task.WaitAsync(TimeSpan.FromSeconds(5)));
}
[Fact]
public async Task AcceptedConnection_CanFetchInitiatorResourceBidirectionally()
{
var fetched = new TaskCompletionSource<IResource>(
TaskCreationOptions.RunContinuationsAsynchronously);
await using var cluster = await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
serverCreated: server => server.ConnectionReady += connection =>
_ = Task.Run(async () =>
{
try
{
fetched.TrySetResult(await connection.Get("client/exported"));
}
catch (Exception exception)
{
fetched.TrySetException(exception);
}
}),
populateClient: async warehouse =>
{
await warehouse.Put("client", new MemoryStore());
await warehouse.Put("client/exported", new Node { Id = 818 });
})
.WaitAsync(TimeSpan.FromSeconds(10));
var proxy = Assert.IsType<EpResource>(
await fetched.Task.WaitAsync(TimeSpan.FromSeconds(5)));
dynamic exported = proxy;
Assert.Equal(818, Convert.ToInt32(exported.Id));
}
[Fact]
public async Task AuthenticatedHandshake_StoresTypedHeadersWithoutAuthenticationData()
{
await using var cluster = await IntegrationCluster
.StartAsync(_ => Task.CompletedTask)
.WaitAsync(TimeSpan.FromSeconds(10));
var serverConnection = Assert.Single(cluster.Server.Connections);
Assert.NotNull(cluster.Connection.Session.LocalHeaders.IPAddress);
Assert.Null(cluster.Connection.Session.RemoteHeaders.AuthenticationData);
Assert.Equal("test", serverConnection.Session.RemoteHeaders.Domain);
Assert.Equal(PasswordAuthenticationProvider.ProtocolName,
serverConnection.Session.RemoteHeaders.AuthenticationProtocol);
Assert.Null(serverConnection.Session.RemoteHeaders.AuthenticationData);
}
[Fact]
public async Task AuthenticatedHandshake_NegotiatesAesAndEncryptsApplicationTraffic()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/encrypted", new Node { Id = 42 });
},
encrypted: true)
.WaitAsync(TimeSpan.FromSeconds(10));
var serverConnection = Assert.Single(cluster.Server.Connections);
Assert.True(cluster.Connection.IsEncrypted);
Assert.True(serverConnection.IsEncrypted);
Assert.Equal(EncryptionMode.EncryptWithSessionKey,
cluster.Connection.Session.EncryptionMode);
Assert.Equal(AesEncryptionProvider.Name,
cluster.Connection.Session.RemoteHeaders.CipherType);
Assert.Equal(AesEncryptionProvider.Name,
serverConnection.Session.LocalHeaders.CipherType);
Assert.NotNull(cluster.Connection.Session.SymetricCipher);
Assert.NotNull(serverConnection.Session.SymetricCipher);
Assert.Null(cluster.Connection.Session.LocalHeaders.CipherKey);
Assert.Null(cluster.Connection.Session.RemoteHeaders.CipherKey);
// Fetch crosses the protected record layer in both directions.
Assert.NotNull(await Task.Run(async () =>
await cluster.Connection.Get("sys/encrypted"))
.WaitAsync(TimeSpan.FromSeconds(10)));
}
[Fact]
public async Task ServerRequiredEncryption_RejectsPlaintextWithoutDowngrade()
{
await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
encrypted: false,
requireEncryption: true)
.WaitAsync(TimeSpan.FromSeconds(10)));
}
[Fact]
public async Task DisallowedAuthenticationProvider_FailsClosedPromptly()
{
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
allowAuthentication: false)
.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.IsNotType<TimeoutException>(exception);
}
[Fact]
public async Task AllowlistedButUnregisteredAuthenticationProvider_FailsClosedPromptly()
{
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
registerServerAuthenticationProvider: false)
.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.IsNotType<TimeoutException>(exception);
}
[Fact]
public async Task AddressBoundEncryption_DerivesMatchingCiphersAcrossPeers()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/address-bound", new Node { Id = 7 });
},
encrypted: true,
encryptionMode: EncryptionMode.EncryptWithSessionKeyAndAddress)
.WaitAsync(TimeSpan.FromSeconds(10));
Assert.True(cluster.Connection.IsEncrypted);
Assert.Equal(EncryptionMode.EncryptWithSessionKeyAndAddress,
cluster.Connection.Session.EncryptionMode);
Assert.NotNull(await Task.Run(async () =>
await cluster.Connection.Get("sys/address-bound"))
.WaitAsync(TimeSpan.FromSeconds(10)));
}
[Fact]
public async Task DisallowedEncryptionProvider_FailsWithoutPlaintextFallback()
{
await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster
.StartAsync(
_ => Task.CompletedTask,
encrypted: true,
allowEncryption: false)
.WaitAsync(TimeSpan.FromSeconds(10)));
}
[Fact]
public async Task OneStepAuthentication_ConfirmsEncryptionBeforeFetchAndRpc()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/one-step-encrypted", new EncryptedEchoResource());
},
encrypted: true,
oneStepAuthentication: true)
.WaitAsync(TimeSpan.FromSeconds(10));
var remote = (EpResource)await Task.Run(async () =>
await cluster.Connection.Get("sys/one-step-encrypted"))
.WaitAsync(TimeSpan.FromSeconds(10));
var function = remote.Instance.Definition
.GetFunctionDefByName(nameof(EncryptedEchoResource.Echo));
var result = await remote._Invoke(
function.Index,
new Map<byte, object> { [0] = 117 });
Assert.True(cluster.Connection.Session.Authenticated);
Assert.True(cluster.Connection.IsEncrypted);
var serverConnection = Assert.Single(cluster.Server.Connections);
Assert.True(serverConnection.Session.Authenticated);
Assert.True(serverConnection.IsEncrypted);
Assert.Equal(117, Convert.ToInt32(result));
}
[Fact]
public async Task RequiredAuthenticationKeyRotation_CompletesBeforeConnectionsBecomeReady()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/key-rotation", new EncryptedEchoResource());
},
encrypted: true,
oneStepAuthentication: true,
requireKeyRotation: true)
.WaitAsync(TimeSpan.FromSeconds(10));
var clientHandler = Assert.IsType<OneStepAuthenticationHandler>(
cluster.Connection.Session.AuthenticationHandler);
var serverConnection = Assert.Single(cluster.Server.Connections);
var serverHandler = Assert.IsType<OneStepAuthenticationHandler>(
serverConnection.Session.AuthenticationHandler);
Assert.True(clientHandler.KeyRotationCompleted);
Assert.True(serverHandler.KeyRotationCompleted);
Assert.True(cluster.Connection.IsEncrypted);
Assert.True(serverConnection.IsEncrypted);
Assert.NotNull(await Task.Run(async () =>
await cluster.Connection.Get("sys/key-rotation"))
.WaitAsync(TimeSpan.FromSeconds(10)));
}
[Fact]
public async Task RequiredAuthenticationKeyRotation_RejectsUnencryptedSession()
{
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster.StartAsync(
_ => Task.CompletedTask,
encrypted: false,
oneStepAuthentication: true,
requireKeyRotation: true)
.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.IsNotType<TimeoutException>(exception);
}
[Fact]
public async Task WebSocketTransport_EncryptsFetchAndRpc()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/websocket-encrypted", new EncryptedEchoResource());
},
encrypted: true,
useWebSocket: true)
.WaitAsync(TimeSpan.FromSeconds(10));
var remote = (EpResource)await Task.Run(async () =>
await cluster.Connection.Get("sys/websocket-encrypted"))
.WaitAsync(TimeSpan.FromSeconds(10));
var function = remote.Instance.Definition
.GetFunctionDefByName(nameof(EncryptedEchoResource.Echo));
var result = await remote._Invoke(
function.Index,
new Map<byte, object> { [0] = 203 });
Assert.True(cluster.Connection.IsEncrypted);
Assert.IsType<FrameworkWebSocket>(cluster.Connection.Socket);
Assert.True(Assert.Single(cluster.Server.Connections).IsEncrypted);
Assert.Equal(203, Convert.ToInt32(result));
}
[Fact]
public async Task AnonymousWebSocketTransport_AssignsSchemaDomain()
{
await using var cluster = await IntegrationCluster
.StartAsync(
async warehouse =>
{
await warehouse.Put("sys/websocket-anonymous", new EncryptedEchoResource());
},
useWebSocket: true,
anonymous: true)
.WaitAsync(TimeSpan.FromSeconds(10));
var remote = (EpResource)await Task.Run(async () =>
await cluster.Connection.Get("sys/websocket-anonymous"))
.WaitAsync(TimeSpan.FromSeconds(10));
var function = remote.Instance.Definition
.GetFunctionDefByName(nameof(EncryptedEchoResource.Echo));
var result = await remote._Invoke(
function.Index,
new Map<byte, object>
{
[0] = 42,
});
Assert.Equal(42, Convert.ToInt32(result));
Assert.False(string.IsNullOrWhiteSpace(
Assert.Single(cluster.Server.Connections).RemoteDomain));
}
[Fact]
public async Task WrongAuthenticatedSessionKey_FailsPromptlyDuringKeyConfirmation()
{
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
await IntegrationCluster.StartAsync(
_ => Task.CompletedTask,
encrypted: true,
oneStepAuthentication: true,
mismatchedSessionKeys: true)
.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.IsNotType<TimeoutException>(exception);
}
[Fact]
public async Task AddressBoundEncryption_WorksAcrossWebSocketWithConcreteEndpoints()
{
await using var cluster = await IntegrationCluster.StartAsync(
_ => Task.CompletedTask,
encrypted: true,
encryptionMode: EncryptionMode.EncryptWithSessionKeyAndAddress,
useWebSocket: true)
.WaitAsync(TimeSpan.FromSeconds(10));
Assert.True(cluster.Connection.IsEncrypted);
Assert.True(Assert.Single(cluster.Server.Connections).IsEncrypted);
Assert.NotEqual(IPAddress.Any, cluster.Connection.RemoteEndPoint.Address);
}
}