CI / build-and-test (push) Canceled after 0s
Ein Sync-Ausfall (offline/Fehler) war bisher nur als blasser Text in der Statuszeile sichtbar und konnte so stundenlang unbemerkt bleiben, bis auf einem anderen Gerät Daten fehlten. SyncStatusViewModel meldet neue Ausfälle jetzt einmalig aktiv per Toast (nicht bei jedem automatischen Retry erneut). DatabaseEncryptionService.IsEncrypted/VerifyPassword öffneten die Datenbankdatei mit einer zweiten, exklusiven (Direct) LiteDB-Verbindung, während LiteDbContext dieselbe Datei bereits im Shared-Modus offen hält - jeder Aufruf (z.B. beim Öffnen der Einstellungen) crashte reproduzierbar mit IOException. Beide öffnen jetzt ebenfalls im Shared-Modus und erzwingen einen tatsächlichen Lesezugriff, da Shared-Verbindungen die Datei lazy öffnen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
6.6 KiB
C#
172 lines
6.6 KiB
C#
using LiteDB;
|
|
using Xunit;
|
|
|
|
namespace LehrerApp.Data.Tests;
|
|
|
|
public sealed class DatabaseEncryptionServiceTests
|
|
{
|
|
[Fact]
|
|
public void IsEncrypted_OhneVorhandeneDatei_GibtFalseZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.IsEncrypted(temp.Path));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsEncrypted_UnverschluesselteDatenbank_GibtFalseZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(temp.Path))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.IsEncrypted(temp.Path));
|
|
}
|
|
|
|
/// Regression: SettingsViewModel ruft IsEncrypted/VerifyPassword mit dem Pfad der bereits
|
|
/// laufenden Datenbank auf, während LiteDbContext deren Shared-Verbindung noch offen hält
|
|
/// (z.B. bei jedem Öffnen der Einstellungen). Eine zweite Verbindung im Standard-Modus
|
|
/// (Direct) würde das mit "The process cannot access the file" quittieren.
|
|
[Fact]
|
|
public void IsEncrypted_WaehrendAndereSharedVerbindungOffenIst_WirftNicht()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(temp.Path))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
using var open = new LiteDatabase(new ConnectionString(temp.Path) { Connection = ConnectionType.Shared });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.IsEncrypted(temp.Path));
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyPassword_WaehrendAndereSharedVerbindungOffenIst_WirftNicht()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(temp.Path))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
var service = new DatabaseEncryptionService();
|
|
service.SetPassword(temp.Path, currentPassword: null, newPassword: "geheim123");
|
|
using var open = new LiteDatabase(new ConnectionString(temp.Path)
|
|
{ Password = "geheim123", Connection = ConnectionType.Shared });
|
|
|
|
Assert.True(service.VerifyPassword(temp.Path, "geheim123"));
|
|
Assert.False(service.VerifyPassword(temp.Path, "falsch"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SetPassword_VerschluesseltDieDatenbankUndBehaeltDieDaten()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(temp.Path))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["Name"] = "Hallo" });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
service.SetPassword(temp.Path, currentPassword: null, newPassword: "geheim123");
|
|
|
|
Assert.True(service.IsEncrypted(temp.Path));
|
|
Assert.True(service.VerifyPassword(temp.Path, "geheim123"));
|
|
Assert.False(service.VerifyPassword(temp.Path, "falsch"));
|
|
|
|
using var reopened = new LiteDatabase(new ConnectionString(temp.Path) { Password = "geheim123" });
|
|
var doc = reopened.GetCollection<BsonDocument>("t").FindAll().First();
|
|
Assert.Equal("Hallo", doc["Name"].AsString);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetPassword_EntferntDasPasswortWiederWennNeuesPasswortNullIst()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
var service = new DatabaseEncryptionService();
|
|
using (var db = new LiteDatabase(new ConnectionString(temp.Path) { Password = "geheim123" }))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
|
|
service.SetPassword(temp.Path, currentPassword: "geheim123", newPassword: null);
|
|
|
|
Assert.False(service.IsEncrypted(temp.Path));
|
|
using var reopened = new LiteDatabase(temp.Path);
|
|
Assert.Single(reopened.GetCollection<BsonDocument>("t").FindAll());
|
|
}
|
|
|
|
[Fact]
|
|
public void CanOpenAndRead_OhneVorhandeneDatei_GibtFalseZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.CanOpenAndRead(temp.Path, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanOpenAndRead_GueltigeUnverschluesselteDatenbank_GibtTrueZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(temp.Path))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.True(service.CanOpenAndRead(temp.Path, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanOpenAndRead_GueltigeVerschluesselteDatenbankMitRichtigemPasswort_GibtTrueZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(new ConnectionString(temp.Path) { Password = "geheim123" }))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.True(service.CanOpenAndRead(temp.Path, "geheim123"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanOpenAndRead_FalschesPasswort_GibtFalseZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
using (var db = new LiteDatabase(new ConnectionString(temp.Path) { Password = "geheim123" }))
|
|
db.GetCollection<BsonDocument>("t").Insert(new BsonDocument { ["x"] = 1 });
|
|
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.CanOpenAndRead(temp.Path, "falsch"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanOpenAndRead_BeschaedigteDatei_GibtFalseZurueck()
|
|
{
|
|
using var temp = new TempDatabase();
|
|
// Eine kurze Textdatei initialisiert LiteDB stillschweigend als neue, leere Datenbank
|
|
// (kleiner als eine Seite) — erst genug Datenmüll jenseits der Seitengröße lässt das
|
|
// Datei-Layout tatsächlich fehlschlagen und simuliert eine echte Beschädigung
|
|
// (z.B. durch einen vollen Datenträger während der Backup-Kopie).
|
|
File.WriteAllBytes(temp.Path, Enumerable.Repeat((byte)0x42, 32_768).ToArray());
|
|
var service = new DatabaseEncryptionService();
|
|
|
|
Assert.False(service.CanOpenAndRead(temp.Path, null));
|
|
}
|
|
|
|
private sealed class TempDatabase : IDisposable
|
|
{
|
|
private readonly string _directory = System.IO.Path.Combine(
|
|
System.IO.Path.GetTempPath(), $"lehrerapp-dbenc-tests-{Guid.NewGuid():N}");
|
|
public string Path { get; }
|
|
|
|
public TempDatabase()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
Path = System.IO.Path.Combine(_directory, "test.db");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
|
}
|
|
}
|
|
}
|