using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using DodoSSH.Api.Features.Relay;
namespace DodoSSH.Api.Tests;
///
/// A resolver and a dialler that reach only what a test has arranged.
///
///
/// Names resolve to what says, and address literals to themselves, as the real
/// resolver does. An address dials the loopback endpoint mapped it to; anything
/// else is refused the way a closed port is, so a test that forgot to arrange a target sees the relay's
/// real "unreachable" path rather than a hang.
///
internal sealed class FakeRelayNetwork : IRelayNetwork
{
private readonly ConcurrentDictionary names = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary routes = new();
private readonly ConcurrentQueue dialled = new();
/// Every endpoint the relay asked to connect to, in order.
internal IReadOnlyCollection Dialled => dialled;
internal void Resolve(string name, params IPAddress[] addresses) => names[name] = addresses;
internal void Route(IPEndPoint target, IPEndPoint loopback) => routes[target] = loopback;
///
public Task ResolveAsync(string host, CancellationToken cancellationToken)
{
if (IPAddress.TryParse(host, out var literal))
{
return Task.FromResult([literal]);
}
return names.TryGetValue(host, out var addresses)
? Task.FromResult(addresses)
: Task.FromException(new SocketException((int)SocketError.HostNotFound));
}
///
public async Task ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken)
{
var endpoint = new IPEndPoint(address, port);
dialled.Enqueue(endpoint);
if (!routes.TryGetValue(endpoint, out var loopback))
{
throw new SocketException((int)SocketError.ConnectionRefused);
}
var client = new TcpClient();
try
{
await client.ConnectAsync(loopback, cancellationToken);
return client.GetStream();
}
catch
{
client.Dispose();
throw;
}
}
}