Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
da4ceac7d5
commit
837a634a22
@@ -10,11 +10,11 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Core.Tests.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Ein ungeprüftes Wiederherstellen ist kein Backup, sondern eine Vermutung.
|
||||
/// Deshalb liegt der Schwerpunkt hier auf dem vollständigen Rundlauf.
|
||||
/// An unchecked restore is not a backup, but a guess.
|
||||
/// That's why the focus here is on the complete round trip.
|
||||
///
|
||||
/// Läuft in <see cref="SecretKeyCollection"/>, weil das Umschreiben der Geheimnisse
|
||||
/// beim Sichern denselben statischen Schlüssel benutzt wie die Geheimnistests.
|
||||
/// Runs in <see cref="SecretKeyCollection"/>, because rewriting the secrets
|
||||
/// when backing up uses the same static key as the secret tests.
|
||||
/// </summary>
|
||||
[Collection(SecretKeyCollection.Name)]
|
||||
public sealed class BackupServiceTests : IDisposable
|
||||
@@ -37,14 +37,14 @@ public sealed class BackupServiceTests : IDisposable
|
||||
{
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
// ─── Aufbau einer realistischen Instanz ───
|
||||
// ─── Building a realistic instance ───
|
||||
|
||||
private async Task BuildInstanceAsync()
|
||||
{
|
||||
// Instanzkonfiguration — Zugangsdaten liegen DPAPI-geschützt wie im Betrieb.
|
||||
// Instance configuration — credentials are DPAPI-protected as in operation.
|
||||
var instanceSettings = $$"""
|
||||
{
|
||||
"instanceId": "test-01",
|
||||
@@ -81,7 +81,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
AtomicFile.WriteAllText(Path.Combine(agentDir, "Workspace", "bericht.md"), "# Bericht\nInhalt.");
|
||||
AtomicFile.WriteAllText(Path.Combine(agentDir, "Logs", "lauf.log"), "viele Zeilen Protokoll");
|
||||
|
||||
// Datenbank mit einer Erinnerung — der wertvollste Teil.
|
||||
// Database with a memory — the most valuable part.
|
||||
var storage = new SqliteStorage(Path.Combine(_instanceDir, "state.db"));
|
||||
var memory = new SqliteMemoryRepository(storage);
|
||||
await memory.RememberAsync(new MemoryEntry
|
||||
@@ -94,7 +94,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
CreatedBy = "analyst"
|
||||
}, default);
|
||||
|
||||
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
|
||||
// Close connections so VACUUM INTO meets a quiet file.
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_instanceDir);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
private string RestoreDir => Path.Combine(_root, "wiederhergestellt");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Der Rundlauf
|
||||
// The round trip
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
@@ -118,17 +118,17 @@ public sealed class BackupServiceTests : IDisposable
|
||||
|
||||
result.Written.ShouldNotBeEmpty();
|
||||
|
||||
// Persönlichkeit
|
||||
// Personality
|
||||
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Identity.md"))
|
||||
.ShouldContain("Der Analyst");
|
||||
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Soul.md"))
|
||||
.ShouldContain("Gründlich");
|
||||
|
||||
// Arbeitsstand
|
||||
// Work state
|
||||
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Workspace", "bericht.md"))
|
||||
.ShouldContain("Inhalt");
|
||||
|
||||
// Datenbank samt Gedächtnis
|
||||
// Database including memory
|
||||
File.Exists(Path.Combine(RestoreDir, "state.db")).ShouldBeTrue();
|
||||
|
||||
var memory = new SqliteMemoryRepository(new SqliteStorage(Path.Combine(RestoreDir, "state.db")));
|
||||
@@ -142,8 +142,8 @@ public sealed class BackupServiceTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Zugangsdaten_sind_nach_dem_Wiederherstellen_wieder_nutzbar()
|
||||
{
|
||||
// Der eigentliche Zweck der Passphrase: Ein Backup wird gebraucht, wenn der
|
||||
// Rechner defekt ist — DPAPI-geschützte Werte wären dann unlesbar.
|
||||
// The actual purpose of the passphrase: a backup is needed when the
|
||||
// machine is broken — DPAPI-protected values would then be unreadable.
|
||||
await BuildInstanceAsync();
|
||||
|
||||
await _service.CreateAsync(_instanceDir, ZipPath,
|
||||
@@ -160,17 +160,17 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Umzug auf einen anderen Rechner — der Migrationspfad aus #190.
|
||||
/// The move to another machine — the migration path from #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>The tests above back up and restore in the same run and
|
||||
/// use the same key in doing so. They therefore cannot show what matters
|
||||
/// when moving: that the values are readable even if the key of the
|
||||
/// source machine does <b>not</b> come along. Exactly that is the case Windows → Linux —
|
||||
/// the existing values are DPAPI-encrypted, and
|
||||
/// <c>SecretProtector.UnprotectV1</c> deliberately throws for them on Linux.</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>
|
||||
/// <para>Here the key therefore changes in the middle of the test. Without the change
|
||||
/// the test would be green without proving anything.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Der_Umzug_auf_einen_anderen_Rechner_traegt()
|
||||
@@ -179,15 +179,15 @@ public sealed class BackupServiceTests : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// Maschine 1: Werte liegen unter dem hiesigen Schlüssel.
|
||||
// Machine 1: values lie under the local key.
|
||||
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.
|
||||
// Machine 2: different machine, different key. The key does not travel
|
||||
// along — it deliberately lies outside the instance directory.
|
||||
SecretKeyStore.UseDirectory(zielMaschine);
|
||||
|
||||
await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "umzug" });
|
||||
@@ -201,7 +201,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
var stored = settings.RootElement.GetProperty("openRouterApiKey").GetString();
|
||||
SecretProtector.Unprotect(stored).ShouldBe(ApiKeyPlain);
|
||||
|
||||
// Auch die verschachtelten Werte, nicht nur der offensichtliche oben.
|
||||
// The nested values too, not only the obvious one at the top.
|
||||
var agent = JsonDocument.Parse(File.ReadAllText(
|
||||
Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "AgentSettings.json")));
|
||||
|
||||
@@ -212,22 +212,22 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
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.
|
||||
// A directory of its own for the next test of the collection: that of the
|
||||
// source machine is no longer available here, and the real
|
||||
// user key must certainly not be it.
|
||||
SecretKeyStore.UseDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* secondary */ }
|
||||
}
|
||||
}
|
||||
|
||||
[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.
|
||||
// The flip side, and the reason why the passphrase has to be offered when backing
|
||||
// up: without it a backup on the target machine is worthless as far as the
|
||||
// credentials are concerned. That is not an error, but the price for the
|
||||
// key not travelling along — but it has to be noticed instead of happening silently.
|
||||
var zielMaschine = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
|
||||
try
|
||||
@@ -246,7 +246,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
{
|
||||
SecretKeyStore.UseDirectory(
|
||||
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
|
||||
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* secondary */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ public sealed class BackupServiceTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Unbekannte_Felder_ueberstehen_den_Rundlauf()
|
||||
{
|
||||
// Eine Sicherung darf nichts wegwerfen, nur weil eine Programmfassung ein
|
||||
// Feld nicht kennt.
|
||||
// A backup must not throw anything away just because a program version does not know
|
||||
// a field.
|
||||
await BuildInstanceAsync();
|
||||
|
||||
await _service.CreateAsync(_instanceDir, ZipPath,
|
||||
@@ -284,7 +284,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Zugangsdaten weglassen
|
||||
// Leaving out credentials
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
@@ -295,7 +295,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
await _service.CreateAsync(_instanceDir, ZipPath,
|
||||
new BackupOptions { Secrets = SecretMode.Exclude });
|
||||
|
||||
// Das gesamte Archiv im Klartext durchsuchen.
|
||||
// Search the entire archive in plain text.
|
||||
var inhalt = ReadWholeArchive(ZipPath);
|
||||
|
||||
inhalt.ShouldNotContain(ApiKeyPlain);
|
||||
@@ -318,7 +318,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Manifest und Fehlerfälle
|
||||
// Manifest and error cases
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
@@ -369,7 +369,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
await BuildInstanceAsync();
|
||||
await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());
|
||||
|
||||
// Eine Datei im Archiv nachträglich verändern.
|
||||
// Change a file in the archive afterwards.
|
||||
using (var archive = ZipFile.Open(ZipPath, ZipArchiveMode.Update))
|
||||
{
|
||||
var entry = archive.Entries.First(e => e.FullName.EndsWith("Identity.md"));
|
||||
@@ -400,7 +400,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Vorschau und Überschreiben
|
||||
// Preview and overwriting
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
@@ -449,7 +449,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Umfang
|
||||
// Scope
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
@@ -477,7 +477,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Die_WAL_Begleitdateien_landen_nicht_im_Archiv()
|
||||
{
|
||||
// VACUUM INTO arbeitet sie bereits ein — mitzusichern wäre irreführend.
|
||||
// VACUUM INTO already incorporates them — backing them up too would be misleading.
|
||||
await BuildInstanceAsync();
|
||||
await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());
|
||||
|
||||
@@ -505,7 +505,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
_instanceDir, ZipPath, new BackupOptions { Secrets = SecretMode.Passphrase }));
|
||||
}
|
||||
|
||||
// ─── Helfer ───
|
||||
// ─── Helpers ───
|
||||
|
||||
private static string ReadWholeArchive(string zipPath)
|
||||
{
|
||||
|
||||
@@ -10,15 +10,15 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Core.Tests.Backup;
|
||||
|
||||
/// <summary>
|
||||
/// Die Sicherung, verschlüsselt mit dem Lizenzschlüssel.
|
||||
/// The backup, encrypted with the license key.
|
||||
///
|
||||
/// <para><b>Worum es geht.</b> Eine Sicherung soll einen Maschinenwechsel überstehen,
|
||||
/// ohne dass eine Passphrase gespeichert oder eingetippt werden muss. Der Lizenzschlüssel
|
||||
/// ist ohnehin da. Und ein Archiv auf einem FTP-Server oder einem USB-Stick soll niemandem
|
||||
/// nützen, der es nur findet.</para>
|
||||
/// <para><b>What this is about.</b> A backup should survive a change of machine
|
||||
/// without a passphrase having to be stored or typed in. The license key
|
||||
/// is there anyway. And an archive on an FTP server or a USB stick should be of no use
|
||||
/// to anyone who merely finds it.</para>
|
||||
///
|
||||
/// <para>Läuft in <see cref="SecretKeyCollection"/>: Der Maschinenwechsel wird über den
|
||||
/// statischen Schlüsselspeicher nachgestellt.</para>
|
||||
/// <para>Runs in <see cref="SecretKeyCollection"/>: the change of machine is reproduced via the
|
||||
/// static key store.</para>
|
||||
/// </summary>
|
||||
[Collection(SecretKeyCollection.Name)]
|
||||
public sealed class LicenseBackupTests : IDisposable
|
||||
@@ -42,7 +42,7 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
SecretKeyStore.UseDirectory(null);
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private string Quelle(byte[] inhalt)
|
||||
@@ -52,11 +52,11 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
return pfad;
|
||||
}
|
||||
|
||||
// ─── Der Behälter ───
|
||||
// ─── The container ───
|
||||
|
||||
/// <summary>
|
||||
/// Mehr als ein Stück: Die Stückelung ist der Teil, an dem ein Fehler Daten
|
||||
/// verschluckt, ohne dass es auffällt.
|
||||
/// More than one chunk: the chunking is the part where an error swallows data
|
||||
/// without it being noticed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ein_Archiv_ueber_mehrere_Stuecke_kommt_unveraendert_zurueck()
|
||||
@@ -79,8 +79,8 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ein anderer Schlüssel wird am Kopf erkannt — bevor irgendetwas entschlüsselt und
|
||||
/// vor allem bevor eine Instanz dafür angehalten wird.
|
||||
/// A different key is recognized at the header — before anything is decrypted and
|
||||
/// above all before an instance is stopped for it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ein_fremder_Schluessel_wird_erkannt_bevor_etwas_geschrieben_wird()
|
||||
@@ -97,8 +97,8 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Im Wiederherstellungsdialog tippt ein Mensch einen fremden Schlüssel ab. Ein
|
||||
/// Leerzeichen oder ein kleiner Buchstabe dürfen dort nicht über „verloren" entscheiden.
|
||||
/// In the restore dialog a human types in a foreign key. A
|
||||
/// space or a lowercase letter must not decide about "lost" there.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Schreibweise_und_Leerzeichen_des_Schluessels_spielen_keine_Rolle()
|
||||
@@ -119,8 +119,8 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fehlt hinten ein ganzes Stück, muss das auffallen. Ohne Endmarke hielte man den Rest
|
||||
/// für vollständig und bekäme ein ZIP, das beim Auspacken irgendwo abbricht.
|
||||
/// If a whole chunk is missing at the end, that must be noticed. Without the end marker you would consider the rest
|
||||
/// complete and get a ZIP that aborts somewhere when unpacking.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ein_fehlendes_letztes_Stueck_faellt_auf()
|
||||
@@ -130,7 +130,7 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
|
||||
LicenseBackupCipher.Encrypt(Quelle(inhalt), manifest, Datei("a.clawdbak"), Lizenz);
|
||||
|
||||
// Kopf 8+1+16, Manifest 12+4+16+len, erstes Stueck 12+4+16+1 MiB - danach kappen.
|
||||
// Header 8+1+16, manifest 12+4+16+len, first chunk 12+4+16+1 MiB - cut after that.
|
||||
var bisErstesStueck = 25 + (32 + Encoding.UTF8.GetByteCount(manifest)) + (32 + 1024 * 1024);
|
||||
var voll = File.ReadAllBytes(Datei("a.clawdbak"));
|
||||
File.WriteAllBytes(Datei("a.clawdbak"), voll[..bisErstesStueck]);
|
||||
@@ -153,7 +153,7 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
LicenseBackupCipher.DecryptTo(Datei("a.clawdbak"), Datei("zurueck.bin"), Lizenz));
|
||||
}
|
||||
|
||||
// ─── Die Sicherung als Ganzes ───
|
||||
// ─── The backup as a whole ───
|
||||
|
||||
private string Instanz()
|
||||
{
|
||||
@@ -185,9 +185,9 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Fall, um den es geht: Sichern auf Maschine A, zurückspielen auf Maschine B, und
|
||||
/// der API-Schlüssel ist danach lesbar — ohne Passphrase, allein mit dem
|
||||
/// Lizenzschlüssel.
|
||||
/// The case this is about: back up on machine A, restore on machine B, and
|
||||
/// the API key is readable afterwards — without a passphrase, solely with the
|
||||
/// license key.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Eine_Sicherung_ueberlebt_den_Maschinenwechsel()
|
||||
@@ -207,8 +207,8 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In der Datei steht nichts im Klartext — weder der Schlüssel noch, dass es überhaupt
|
||||
/// einen gibt.
|
||||
/// Nothing is in plain text in the file — neither the key nor that there is
|
||||
/// one at all.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Die_Sicherung_verraet_keinen_Klartext()
|
||||
@@ -224,7 +224,7 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mit fremdem Schlüssel wird nichts geschrieben — nicht halb, nicht teilweise.
|
||||
/// With a foreign key nothing is written — not half, not partially.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Mit_fremdem_Schluessel_bleibt_das_Ziel_unberuehrt()
|
||||
|
||||
Reference in New Issue
Block a user