Simulierten Testzugang für interne Wizard-Tests ohne echtes Gerät

DemoRouterOsClient simuliert ein RouterOS-Gerät komplett im Arbeitsspeicher
(keine Netzwerkverbindung) und wird aktiviert, wenn exakt die Zugangsdaten
127.127.127.127 / admin / admin eingegeben werden. So lässt sich der
komplette Interview-Ablauf durchklicken, ohne ein reales MikroTik-Gerät
zu verändern oder überhaupt zu benötigen.

Nur in Debug-Builds erreichbar (#if DEBUG in RouterOsClientFactory),
damit dieser Testzugang nicht in der für Endnutzer veröffentlichten
Version landet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-11 19:22:08 +02:00
1 parent ca21d7b8a0
commit f23da2e981
3 files changed
+146

No files matched your search

@@ -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<string, string> { ["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);
}
}
@@ -0,0 +1,95 @@
namespace MikrotikInstaller.Core.Connectivity.Demo;
/// <summary>
/// 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
/// (<see cref="Matches"/>) — es findet dabei keinerlei Netzwerkverbindung statt, alle Daten
/// liegen nur im Arbeitsspeicher dieses Objekts. Nur in Debug-Builds erreichbar
/// (siehe <see cref="RouterOsClientFactory"/>), damit dieser Testzugang nicht in der
/// veröffentlichten Version landet.
/// </summary>
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<string, List<Dictionary<string, string>>> _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<IReadOnlyList<IReadOnlyDictionary<string, string>>> GetAsync(string path, CancellationToken cancellationToken = default)
{
var rows = _data.TryGetValue(Normalize(path), out var list) ? list : [];
IReadOnlyList<IReadOnlyDictionary<string, string>> result = rows.Select(r => (IReadOnlyDictionary<string, string>)r).ToList();
return Task.FromResult(result);
}
public Task<string> AddAsync(string path, IReadOnlyDictionary<string, string> parameters, CancellationToken cancellationToken = default)
{
var id = $"*{_nextId++}";
var row = new Dictionary<string, string>(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<string, string> 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<string, string>? parameters = null, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
private static string Normalize(string path) => path.TrimEnd('/');
private static Dictionary<string, List<Dictionary<string, string>>> 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" },
],
};
}
@@ -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<IRouterOsClient> 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);