diff --git a/src/MikrotikInstaller.Core.Tests/Connectivity/DemoRouterOsClientTests.cs b/src/MikrotikInstaller.Core.Tests/Connectivity/DemoRouterOsClientTests.cs new file mode 100644 index 0000000..28e2bf4 --- /dev/null +++ b/src/MikrotikInstaller.Core.Tests/Connectivity/DemoRouterOsClientTests.cs @@ -0,0 +1,40 @@ +using MikrotikInstaller.Core.Connectivity; +using MikrotikInstaller.Core.Connectivity.Demo; + +namespace MikrotikInstaller.Core.Tests.Connectivity; + +public class DemoRouterOsClientTests +{ + [Fact] + public void Matches_OnlyExactTestCredentials() + { + Assert.True(DemoRouterOsClient.Matches(new RouterOsCredentials("127.127.127.127", "admin", "admin"))); + Assert.False(DemoRouterOsClient.Matches(new RouterOsCredentials("192.168.88.1", "admin", "admin"))); + Assert.False(DemoRouterOsClient.Matches(new RouterOsCredentials("127.127.127.127", "admin", "falsch"))); + } + + [Fact] + public async Task GetAsync_ReturnsSimulatedInterfacesAndResource() + { + var client = new DemoRouterOsClient(); + + var interfaces = await client.GetAsync("/interface"); + var resource = await client.GetAsync("/system/resource"); + + Assert.NotEmpty(interfaces); + Assert.Contains(interfaces, i => i["name"] == "ether1"); + Assert.Single(resource); + } + + [Fact] + public async Task AddAsync_StoresRowAndReturnsId() + { + var client = new DemoRouterOsClient(); + + var id = await client.AddAsync("/interface/bridge", new Dictionary { ["name"] = "bridge-lan" }); + var rows = await client.GetAsync("/interface/bridge"); + + Assert.False(string.IsNullOrEmpty(id)); + Assert.Contains(rows, r => r["name"] == "bridge-lan" && r[".id"] == id); + } +} diff --git a/src/MikrotikInstaller.Core/Connectivity/Demo/DemoRouterOsClient.cs b/src/MikrotikInstaller.Core/Connectivity/Demo/DemoRouterOsClient.cs new file mode 100644 index 0000000..b402acb --- /dev/null +++ b/src/MikrotikInstaller.Core/Connectivity/Demo/DemoRouterOsClient.cs @@ -0,0 +1,95 @@ +namespace MikrotikInstaller.Core.Connectivity.Demo; + +/// +/// Simulierter RouterOS-Client für interne Tests des Assistenten, ganz ohne echtes Gerät. +/// Wird ausschließlich aktiviert, wenn exakt die Test-Zugangsdaten verwendet werden +/// () — es findet dabei keinerlei Netzwerkverbindung statt, alle Daten +/// liegen nur im Arbeitsspeicher dieses Objekts. Nur in Debug-Builds erreichbar +/// (siehe ), damit dieser Testzugang nicht in der +/// veröffentlichten Version landet. +/// +public sealed class DemoRouterOsClient : IRouterOsClient +{ + public const string DemoHost = "127.127.127.127"; + public const string DemoUsername = "admin"; + public const string DemoPassword = "admin"; + + private readonly Dictionary>> _data = BuildInitialData(); + private int _nextId = 1; + + public static bool Matches(RouterOsCredentials credentials) => + credentials.Host == DemoHost && credentials.Username == DemoUsername && credentials.Password == DemoPassword; + + public RouterOsProtocol Protocol => RouterOsProtocol.Rest; + + public Task>> GetAsync(string path, CancellationToken cancellationToken = default) + { + var rows = _data.TryGetValue(Normalize(path), out var list) ? list : []; + IReadOnlyList> result = rows.Select(r => (IReadOnlyDictionary)r).ToList(); + return Task.FromResult(result); + } + + public Task AddAsync(string path, IReadOnlyDictionary parameters, CancellationToken cancellationToken = default) + { + var id = $"*{_nextId++}"; + var row = new Dictionary(parameters) { [".id"] = id }; + + var key = Normalize(path); + if (!_data.TryGetValue(key, out var list)) + { + list = []; + _data[key] = list; + } + + list.Add(row); + return Task.FromResult(id); + } + + public Task SetAsync(string path, string id, IReadOnlyDictionary parameters, CancellationToken cancellationToken = default) + { + if (_data.TryGetValue(Normalize(path), out var list)) + { + var row = list.FirstOrDefault(r => r.GetValueOrDefault(".id") == id); + if (row is not null) + { + foreach (var (key, value) in parameters) + { + row[key] = value; + } + } + } + + return Task.CompletedTask; + } + + public Task RemoveAsync(string path, string id, CancellationToken cancellationToken = default) + { + if (_data.TryGetValue(Normalize(path), out var list)) + { + list.RemoveAll(r => r.GetValueOrDefault(".id") == id); + } + + return Task.CompletedTask; + } + + public Task ExecuteAsync(string path, IReadOnlyDictionary? parameters = null, CancellationToken cancellationToken = default) => + Task.CompletedTask; + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + private static string Normalize(string path) => path.TrimEnd('/'); + + private static Dictionary>> BuildInitialData() => new() + { + ["/system/resource"] = [new() { ["board-name"] = "CHR (Simulator)", ["version"] = "7.15.2 (stable)", ["architecture-name"] = "x86_64" }], + ["/system/identity"] = [new() { ["name"] = "Test-Router" }], + ["/system/routerboard"] = [new() { ["routerboard"] = "no" }], + ["/interface"] = + [ + new() { [".id"] = "*1", ["name"] = "ether1", ["type"] = "ether", ["running"] = "true", ["disabled"] = "false" }, + new() { [".id"] = "*2", ["name"] = "ether2", ["type"] = "ether", ["running"] = "true", ["disabled"] = "false" }, + new() { [".id"] = "*3", ["name"] = "ether3", ["type"] = "ether", ["running"] = "false", ["disabled"] = "false" }, + new() { [".id"] = "*4", ["name"] = "wlan1", ["type"] = "wlan", ["running"] = "false", ["disabled"] = "true" }, + ], + }; +} diff --git a/src/MikrotikInstaller.Core/Connectivity/RouterOsClientFactory.cs b/src/MikrotikInstaller.Core/Connectivity/RouterOsClientFactory.cs index 563f7b9..49a49a3 100644 --- a/src/MikrotikInstaller.Core/Connectivity/RouterOsClientFactory.cs +++ b/src/MikrotikInstaller.Core/Connectivity/RouterOsClientFactory.cs @@ -1,4 +1,5 @@ using MikrotikInstaller.Core.Connectivity.Binary; +using MikrotikInstaller.Core.Connectivity.Demo; using MikrotikInstaller.Core.Connectivity.Rest; namespace MikrotikInstaller.Core.Connectivity; @@ -12,6 +13,16 @@ public static class RouterOsClientFactory { public static async Task ConnectAsync(RouterOsCredentials credentials, CancellationToken cancellationToken = default) { +#if DEBUG + // Nur in Debug-Builds: interner Testzugang ohne echtes Gerät, siehe DemoRouterOsClient. + // Bewusst per Compile-Flag ausgeschlossen, damit dieser Zugang nicht in der für + // Endnutzer veröffentlichten Version landet. + if (DemoRouterOsClient.Matches(credentials)) + { + return new DemoRouterOsClient(); + } +#endif + try { return await RestRouterOsClient.ConnectAsync(credentials, cancellationToken);