Module im WebUI aktivieren und deaktivieren
Umsetzung von #560. Die Ansicht "Moduleinstellungen" zeigte bisher nur an, ob ein Modul laeuft. Jetzt laesst sich dort wie im Modul-Tab des Desktops festlegen, welche Module beim naechsten Start geladen werden. POST /api/modules/{name}/enabled nimmt den gewuenschten Zustand ausdruecklich entgegen ({"enabled": true|false}) und schaltet nicht einfach um. Zwei Klicks aus zwei Fenstern heben sich damit nicht auf, und ein wiederholter Aufruf aendert nichts mehr. Fehlerfaelle: - Unbekanntes Modul: 404. - Modul mit Aktivierungs-Blocker: 409. - Fehlender Zustand: 400. Die Pruefung war zuerst lueckenhaft, der Test hat es gefunden: Mit "bool Enabled" machte ein leerer Rumpf {} daraus false, und ein unvollstaendiger Aufruf haette das Modul deaktiviert. Gemeinsame Quelle statt Kopien: - KnownModules: Die Modulliste stand viermal im Code (Program, CoreViews, ModuleViews, hartkodiert im WebUI). Der Start registriert jetzt die eine Liste. - ModuleActivationView: Die Statusregeln des Desktops ("Aktiv - stoppt nach Neustart" usw.) liegen im Core. Desktop und WebUI zeigen dasselbe. ServerSettingsFile - gefunden, bevor das WebUI in die Datei schreiben durfte: ServerSettings.Load verschluckt jeden Lesefehler und liefert Vorgaben. "Frisch lesen, ein Feld aendern, speichern" haette eine gesperrte oder beschaedigte server_settings.xml durch Vorgaben ersetzt, und mit ihr Lizenzschluessel, Watchdog-Token, Mullvad-Account und Alchemy-Key. Genau so schrieb auch der Modul-Schalter des Desktops. ServerSettingsFile: - liest streng und schreibt bei unlesbarer Datei nichts, - schreibt atomar ueber eine Nachbardatei, - erhaelt unter Linux die Dateirechte, - serialisiert Aenderungen prozessweit. Der Desktop-Schalter benutzt ihn jetzt ebenfalls. Tests: Lizenzschluessel, Token und Alchemy-Key bleiben beim Umschalten in der Datei; eine kaputte Datei bleibt Byte fuer Byte unveraendert und die Anzeige bietet dann kein Umschalten an; Blocker, 404, 400, Idempotenz; die Statustabelle; strenges Lesen, atomares Schreiben und Rechte am Dateiobjekt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using PolyTrader.Core.Configuration;
|
||||
using PolyTraderSharp.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Fortschreiben der server_settings.xml zur Laufzeit (#560). Der Kern ist, was NICHT passieren darf:
|
||||
/// dass eine unlesbare Datei still durch Vorgaben ersetzt wird und gespeicherte Schlüssel verschwinden.
|
||||
/// </summary>
|
||||
public sealed class ServerSettingsFileTests : IDisposable
|
||||
{
|
||||
private readonly string _dir = Path.Combine(Path.GetTempPath(), "polytrader-settings-" + Guid.NewGuid().ToString("N"));
|
||||
private string SettingsPath => Path.Combine(_dir, "server_settings.xml");
|
||||
|
||||
public ServerSettingsFileTests() => Directory.CreateDirectory(_dir);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fehlende_Datei_liefert_Vorgaben_und_wird_beim_Schreiben_angelegt()
|
||||
{
|
||||
var file = new ServerSettingsFile(SettingsPath);
|
||||
|
||||
Assert.Empty(file.Read().DisabledModules);
|
||||
|
||||
file.Update(s => s.DisabledModules.Add("Supervisor"));
|
||||
|
||||
Assert.Equal(new[] { "Supervisor" }, ServerSettings.Load(SettingsPath).DisabledModules);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_aendert_nur_das_eine_Feld()
|
||||
{
|
||||
new ServerSettings { LicenseKey = "enc:v1:L", WatchdogToken = "enc:v1:W", MullvadAccount = "1234", VpnLocation = "cz" }
|
||||
.Save(SettingsPath);
|
||||
|
||||
new ServerSettingsFile(SettingsPath).Update(s => s.VpnLocation = "se");
|
||||
|
||||
var written = ServerSettings.Load(SettingsPath);
|
||||
Assert.Equal("se", written.VpnLocation);
|
||||
Assert.Equal("enc:v1:L", written.LicenseKey);
|
||||
Assert.Equal("enc:v1:W", written.WatchdogToken);
|
||||
Assert.Equal("1234", written.MullvadAccount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unlesbare_Datei_wird_nicht_ueberschrieben()
|
||||
{
|
||||
const string kaputt = "<ServerSettings><LicenseKey>enc:v1:L</LicenseKey><abgeschnitten";
|
||||
File.WriteAllText(SettingsPath, kaputt);
|
||||
var file = new ServerSettingsFile(SettingsPath);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => file.Read());
|
||||
var ex = Assert.Throws<InvalidDataException>(() => file.Update(s => s.VpnLocation = "se"));
|
||||
|
||||
Assert.Contains("nicht lesbar", ex.Message);
|
||||
Assert.Equal(kaputt, File.ReadAllText(SettingsPath));
|
||||
|
||||
// Zum Vergleich das Verhalten, vor dem das schützt: Load liefert still Vorgaben.
|
||||
Assert.Equal(string.Empty, ServerSettings.Load(SettingsPath).LicenseKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kein_Rest_einer_Zwischendatei_bleibt_liegen()
|
||||
{
|
||||
var file = new ServerSettingsFile(SettingsPath);
|
||||
file.Update(s => s.VpnLocation = "se");
|
||||
file.Update(s => s.VpnLocation = "de");
|
||||
|
||||
Assert.Equal(new[] { "server_settings.xml" }, Directory.GetFiles(_dir).Select(Path.GetFileName).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dateirechte_bleiben_unter_Linux_erhalten()
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) return; // NTFS-Rechte vererben sich, dort ist nichts zu tun
|
||||
|
||||
new ServerSettings().Save(SettingsPath);
|
||||
File.SetUnixFileMode(SettingsPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
||||
|
||||
new ServerSettingsFile(SettingsPath).Update(s => s.VpnLocation = "se");
|
||||
|
||||
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(SettingsPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user