Files
RichardandClaude Opus 5 dd8da3fd3f Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell
Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt,
welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen.

Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json
ueberschneiden und darum gemeinsam abgelegt werden:

Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig):
- Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert
- DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli
- WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt
  (version, os, checks, metrics, status stopped)
- Security: MasterKeyResolver, SecretRedactor, FilePermissions
- Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin)
- deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben
  ueber .gitignore aussen vor
- setup.json fuer die Erstinstallation
- UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md
  verweist auf den neuen Plan

Einfenster-Shell (UI-Redesign):
- ShellWindow + ShellNavModel als Seitenleisten-Shell
- WindowMenuBar und WindowMenuModel entfallen
- Modul- und Kernfenster auf die Shell-Einbettung angepasst

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:42:22 +02:00

116 lines
4.2 KiB
C#

using System;
using System.IO;
using System.Runtime.InteropServices;
using PolyTrader.Core.Security;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für D-5/D-10 (Master-Key-Erzeugung bei der Erstinstallation). Der wichtigste
/// Fall ist der negative: ein vorhandener Schlüssel darf NIEMALS überschrieben werden - das
/// würde jedes bereits verschlüsselte Secret in der Datenbank unwiederbringlich unlesbar machen.
/// </summary>
public class MasterKeyResolverTests : IDisposable
{
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "pt-masterkey-tests-" + Guid.NewGuid().ToString("N"));
public MasterKeyResolverTests() => Directory.CreateDirectory(_tempDir);
public void Dispose()
{
try { Directory.Delete(_tempDir, recursive: true); } catch { /* Best effort */ }
}
private string KeyFile => Path.Combine(_tempDir, "master.key");
[Fact]
public void Resolve_prefers_environment_variable_and_never_touches_the_file()
{
string? result = MasterKeyResolver.Resolve(KeyFile, () => "env-key-value");
Assert.Equal("env-key-value", result);
Assert.False(File.Exists(KeyFile));
}
[Fact]
public void Resolve_reads_existing_file_without_modifying_it()
{
File.WriteAllText(KeyFile, "existing-key-value");
var originalWriteTime = File.GetLastWriteTimeUtc(KeyFile);
string? result = MasterKeyResolver.Resolve(KeyFile, () => null);
Assert.Equal("existing-key-value", result);
Assert.Equal(originalWriteTime, File.GetLastWriteTimeUtc(KeyFile));
}
[Fact]
public void Resolve_generates_a_new_key_when_neither_variable_nor_file_exist()
{
bool generatedCallbackFired = false;
string? result = MasterKeyResolver.Resolve(KeyFile, () => null, onGenerated: _ => generatedCallbackFired = true);
Assert.False(string.IsNullOrWhiteSpace(result));
Assert.True(File.Exists(KeyFile));
Assert.True(generatedCallbackFired);
Assert.Equal(result, File.ReadAllText(KeyFile).Trim());
}
[Fact]
public void Resolve_generated_key_decodes_as_a_256_bit_key()
{
string? result = MasterKeyResolver.Resolve(KeyFile, () => null);
byte[] bytes = Convert.FromBase64String(result!);
Assert.Equal(32, bytes.Length);
}
[Fact]
public void Resolve_never_regenerates_once_a_file_exists()
{
string? first = MasterKeyResolver.Resolve(KeyFile, () => null);
string? second = MasterKeyResolver.Resolve(KeyFile, () => null);
string? third = MasterKeyResolver.Resolve(KeyFile, () => null);
Assert.Equal(first, second);
Assert.Equal(first, third);
}
[Fact]
public void Resolve_does_not_generate_when_environment_variable_is_set_even_without_a_file()
{
_ = MasterKeyResolver.Resolve(KeyFile, () => "from-env");
Assert.False(File.Exists(KeyFile));
}
[Fact]
public void Resolve_restricts_generated_file_to_owner_on_unix()
{
// Kein separates Skip-Paket eingebunden - unter Windows greift ohnehin die
// NTFS-Vererbung des Profils (siehe FilePermissions), dort gibt es nichts zu prüfen.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
MasterKeyResolver.Resolve(KeyFile, () => null);
var mode = File.GetUnixFileMode(KeyFile);
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, mode);
}
[Fact]
public void Resolve_returns_null_and_warns_instead_of_throwing_when_generation_fails()
{
// Ein Pfad in einem nicht existierenden Verzeichnis lässt File.WriteAllText scheitern.
string badPath = Path.Combine(_tempDir, "does-not-exist", "master.key");
string? warning = null;
string? result = MasterKeyResolver.Resolve(badPath, () => null, onWarning: msg => warning = msg);
Assert.Null(result);
Assert.NotNull(warning);
}
}
}