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; } } }