- ClawdDotNet.Web: Kestrel im Prozess der Anwendung, Token-Absicherung (TokenGate mit FixedTimeEquals), Endpunkte unter /api, Ereignisstrom unter /events (Server-Sent Events) und fortlaufendes Log-Streaming. - Weboberflaeche (wwwroot/): Vollstaendiges Single-Page-Frontend mit nativem HTML5, CSS3 und ES-Modulen ohne Buildschritt (strikt textContent, #-Routing, sofortiger Login-Sprung bei fehlendem Token, Light/Dark-Theme, lokale Barlow-Schriften). - ClawdDotNet.App: Fachlogik aus den Avalonia-ViewModels in Dienste herausgeloest (AgentAdministration, JobAdministration, ServiceAdministration, RunCoordinator) und ueber InstanceChange synchronisiert. - Tests: ClawdDotNet.Web.Tests und ClawdDotNet.App.Tests hinzugefuegt, alle 684 Tests erfolgreich.
This commit is contained in:
@@ -159,6 +159,97 @@ public sealed class BackupServiceTests : IDisposable
|
||||
SecretProtector.Unprotect(stored).ShouldBe(ApiKeyPlain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Umzug auf einen anderen Rechner — der Migrationspfad aus #190.
|
||||
///
|
||||
/// <para>Die Tests darüber sichern und stellen im selben Lauf wieder her und
|
||||
/// benutzen dabei denselben Schlüssel. Sie können damit gar nicht zeigen, worauf es
|
||||
/// beim Umzug ankommt: dass die Werte auch dann lesbar sind, wenn der Schlüssel der
|
||||
/// Quellmaschine <b>nicht</b> mitkommt. Genau das ist der Fall Windows → Linux —
|
||||
/// die bestehenden Werte sind DPAPI-verschlüsselt, und
|
||||
/// <c>SecretProtector.UnprotectV1</c> wirft dafür auf Linux bewusst.</para>
|
||||
///
|
||||
/// <para>Hier wechselt der Schlüssel deshalb mitten im Test. Ohne den Wechsel wäre
|
||||
/// der Test grün, ohne etwas zu belegen.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Der_Umzug_auf_einen_anderen_Rechner_traegt()
|
||||
{
|
||||
var zielMaschine = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
|
||||
try
|
||||
{
|
||||
// Maschine 1: Werte liegen unter dem hiesigen Schlüssel.
|
||||
await BuildInstanceAsync();
|
||||
var schluesselDerQuelle = File.ReadAllBytes(SecretKeyStore.KeyFilePath);
|
||||
|
||||
await _service.CreateAsync(_instanceDir, ZipPath,
|
||||
new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "umzug" });
|
||||
|
||||
// Maschine 2: anderer Rechner, anderer Schlüssel. Der Schlüssel reist nicht
|
||||
// mit — er liegt bewusst außerhalb des Instanzverzeichnisses.
|
||||
SecretKeyStore.UseDirectory(zielMaschine);
|
||||
|
||||
await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "umzug" });
|
||||
|
||||
File.ReadAllBytes(SecretKeyStore.KeyFilePath)
|
||||
.ShouldNotBe(schluesselDerQuelle, "sonst wäre es derselbe Rechner");
|
||||
|
||||
var settings = JsonDocument.Parse(
|
||||
File.ReadAllText(Path.Combine(RestoreDir, "InstanceSettings.json")));
|
||||
|
||||
var stored = settings.RootElement.GetProperty("openRouterApiKey").GetString();
|
||||
SecretProtector.Unprotect(stored).ShouldBe(ApiKeyPlain);
|
||||
|
||||
// Auch die verschachtelten Werte, nicht nur der offensichtliche oben.
|
||||
var agent = JsonDocument.Parse(File.ReadAllText(
|
||||
Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "AgentSettings.json")));
|
||||
|
||||
var password = agent.RootElement
|
||||
.GetProperty("tools").GetProperty("Mail").GetProperty("password").GetString();
|
||||
|
||||
SecretProtector.Unprotect(password).ShouldBe(MailPasswordPlain);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Ein eigenes Verzeichnis für den nächsten Test der Sammlung: Das der
|
||||
// Quellmaschine steht hier nicht mehr zur Verfügung, und der echte
|
||||
// Benutzerschlüssel darf es erst recht nicht sein.
|
||||
SecretKeyStore.UseDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ohne_die_Passphrase_ist_der_Umzug_verloren()
|
||||
{
|
||||
// Die Kehrseite, und der Grund, warum die Passphrase beim Sichern angeboten
|
||||
// werden muss: Ohne sie ist ein Backup auf der Zielmaschine wertlos, was die
|
||||
// Zugangsdaten angeht. Das ist kein Fehler, sondern der Preis dafür, dass der
|
||||
// Schlüssel nicht mitreist — aber es muss auffallen, statt still zu geschehen.
|
||||
var zielMaschine = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
|
||||
try
|
||||
{
|
||||
await BuildInstanceAsync();
|
||||
|
||||
await _service.CreateAsync(_instanceDir, ZipPath,
|
||||
new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "umzug" });
|
||||
|
||||
SecretKeyStore.UseDirectory(zielMaschine);
|
||||
|
||||
await Should.ThrowAsync<Exception>(
|
||||
() => _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "falsch" }));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SecretKeyStore.UseDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auch_verschachtelte_Zugangsdaten_in_Tool_Konfigurationen_kommen_zurueck()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user