From b366a45f5b02c241cd9c8f2e9efc1acf1a7a14bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:33:45 +0200 Subject: [PATCH] fix: make stateless members static and internalize app-only types (CA1822, CA1515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- src/LageBuch.App/AppPaths.cs | 2 +- src/LageBuch.App/LageBuch.App.csproj | 4 + .../Services/IncidentHostController.cs | 2 +- .../StorageProviderFileDialogService.cs | 2 +- .../Services/SystemAlarmService.cs | 2 +- .../Services/IncidentStore.cs | 7 +- .../Services/MasterDataProvider.cs | 7 +- .../ViewModels/AboutViewModel.cs | 5 ++ .../IncidentRepository.cs | 8 +- .../MasterData/MasterDataStore.cs | 4 +- .../TestAppBuilder.cs | 2 +- tests/LageBuch.Documents.Tests/PdfAssert.cs | 2 +- .../IncidentCreationTests.cs | 2 +- .../ClosedIncidentReadOnlyTests.cs | 4 +- .../CoMeasurementPersistenceTests.cs | 11 ++- .../IncidentRepositoryLoadTests.cs | 4 +- .../IncidentRepositorySaveTests.cs | 16 ++-- .../IncidentRoundTripTests.cs | 52 +++++------ .../IncidentStateProbeTests.cs | 12 +-- .../MasterDataStoreTests.cs | 90 +++++++++---------- .../MigrationForwardCompatTests.cs | 5 +- 21 files changed, 120 insertions(+), 123 deletions(-) diff --git a/src/LageBuch.App/AppPaths.cs b/src/LageBuch.App/AppPaths.cs index 48937db..5195702 100644 --- a/src/LageBuch.App/AppPaths.cs +++ b/src/LageBuch.App/AppPaths.cs @@ -1,6 +1,6 @@ namespace LageBuch.App; -public static class AppPaths +internal static class AppPaths { public static string AppDataDir => GetAppDataDir(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); diff --git a/src/LageBuch.App/LageBuch.App.csproj b/src/LageBuch.App/LageBuch.App.csproj index fe0c7a4..1cdc692 100644 --- a/src/LageBuch.App/LageBuch.App.csproj +++ b/src/LageBuch.App/LageBuch.App.csproj @@ -30,4 +30,8 @@ + + + + diff --git a/src/LageBuch.App/Services/IncidentHostController.cs b/src/LageBuch.App/Services/IncidentHostController.cs index ffad321..a18cee3 100644 --- a/src/LageBuch.App/Services/IncidentHostController.cs +++ b/src/LageBuch.App/Services/IncidentHostController.cs @@ -14,7 +14,7 @@ namespace LageBuch.App.Services; /// reachable over loopback, the LAN, and a tailnet at once. Lives in the desktop head so ASP.NET /// Core stays out of the cross-platform AppLogic/Android build. /// -public sealed class IncidentHostController : IIncidentHostController +internal sealed class IncidentHostController : IIncidentHostController { private readonly IClock _clock; private readonly string _appVersion; diff --git a/src/LageBuch.App/Services/StorageProviderFileDialogService.cs b/src/LageBuch.App/Services/StorageProviderFileDialogService.cs index ebad125..1b0c480 100644 --- a/src/LageBuch.App/Services/StorageProviderFileDialogService.cs +++ b/src/LageBuch.App/Services/StorageProviderFileDialogService.cs @@ -6,7 +6,7 @@ namespace LageBuch.App.Services; -public sealed class StorageProviderFileDialogService : IFileDialogService +internal sealed class StorageProviderFileDialogService : IFileDialogService { private static readonly FilePickerFileType Incident = new("Einsatzdokumentation") { Patterns = new[] { "*.fwincident" } }; diff --git a/src/LageBuch.App/Services/SystemAlarmService.cs b/src/LageBuch.App/Services/SystemAlarmService.cs index 0fac673..a642b27 100644 --- a/src/LageBuch.App/Services/SystemAlarmService.cs +++ b/src/LageBuch.App/Services/SystemAlarmService.cs @@ -14,7 +14,7 @@ namespace LageBuch.App.Services; /// Every path degrades to a silent no-op when the asset or the player is missing, so a build with /// no voice clip yet — or a host without the CLI player — simply stays quiet rather than crashing. /// -public sealed class SystemAlarmService : IAlarmService +internal sealed class SystemAlarmService : IAlarmService { private const uint SndAsync = 0x0001; // play asynchronously private const uint SndNodefault = 0x0002; // no default beep if it fails diff --git a/src/LageBuch.AppLogic/Services/IncidentStore.cs b/src/LageBuch.AppLogic/Services/IncidentStore.cs index 410055d..270baf5 100644 --- a/src/LageBuch.AppLogic/Services/IncidentStore.cs +++ b/src/LageBuch.AppLogic/Services/IncidentStore.cs @@ -5,14 +5,13 @@ namespace LageBuch.AppLogic.Services; public sealed class IncidentStore : IIncidentStore { - private readonly IncidentRepository _repository = new(); private readonly IIncidentFileStore _fileStore = new IncidentFileStore(); - public void Save(string path, Incident incident) => _repository.Save(path, incident); + public void Save(string path, Incident incident) => IncidentRepository.Save(path, incident); - public Incident Load(string path) => _repository.Load(path); + public Incident Load(string path) => IncidentRepository.Load(path); - public IncidentState? TryReadState(string path) => _repository.TryReadState(path); + public IncidentState? TryReadState(string path) => IncidentRepository.TryReadState(path); public void SaveFileBytes(string path, string storageFileName, byte[] bytes) => _fileStore.SaveBytes(path, storageFileName, bytes); diff --git a/src/LageBuch.AppLogic/Services/MasterDataProvider.cs b/src/LageBuch.AppLogic/Services/MasterDataProvider.cs index b50aa4c..61db4a4 100644 --- a/src/LageBuch.AppLogic/Services/MasterDataProvider.cs +++ b/src/LageBuch.AppLogic/Services/MasterDataProvider.cs @@ -5,18 +5,17 @@ namespace LageBuch.AppLogic.Services; public sealed class MasterDataProvider : IMasterDataProvider { private readonly string _path; - private readonly MasterDataStore _store = new(); private MasterDataSet? _cached; public MasterDataProvider(string masterDataPath) => _path = masterDataPath; - public MasterDataSet Get() => _cached ??= _store.GetOrCreate(_path); + public MasterDataSet Get() => _cached ??= MasterDataStore.GetOrCreate(_path); public void Save(MasterDataSet set) { - _store.Save(_path, set); + MasterDataStore.Save(_path, set); // Re-read rather than trust the in-memory copy: the store is the canonical shape // (e.g. personnel comes back name-sorted), so callers see exactly what a fresh start would. - _cached = _store.GetOrCreate(_path); + _cached = MasterDataStore.GetOrCreate(_path); } } diff --git a/src/LageBuch.AppLogic/ViewModels/AboutViewModel.cs b/src/LageBuch.AppLogic/ViewModels/AboutViewModel.cs index 2f1e804..949319a 100644 --- a/src/LageBuch.AppLogic/ViewModels/AboutViewModel.cs +++ b/src/LageBuch.AppLogic/ViewModels/AboutViewModel.cs @@ -23,13 +23,18 @@ public AboutViewModel(IFileDialogService dialogs, string version) Version = version; } + [SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")] public string AppName => "Lagebuch"; + [SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")] public string Descriptor => "Einsatzdokumentation"; public string Version { get; } + [SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")] public string RepositoryUrl => RepoUrl; // Kept in sync with the LICENSE file in the repo root. + [SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")] public string LicenseLine => "Veröffentlicht unter der MIT-Lizenz."; + [SuppressMessage("Performance", "CA1822", Justification = "XAML {Binding} target in AboutView; binding requires an instance property.")] public string CopyrightLine => "Copyright © 2026 Thomas Müller"; [ObservableProperty] diff --git a/src/LageBuch.Persistence/IncidentRepository.cs b/src/LageBuch.Persistence/IncidentRepository.cs index 5c6c2c3..ab74970 100644 --- a/src/LageBuch.Persistence/IncidentRepository.cs +++ b/src/LageBuch.Persistence/IncidentRepository.cs @@ -10,7 +10,7 @@ public sealed class IncidentRepository { private const string Iso = "O"; - public void Save(string path, Incident incident) + public static void Save(string path, Incident incident) { ArgumentNullException.ThrowIfNull(incident); using var cn = SqliteConnectionFactory.OpenReadWrite(path); @@ -269,9 +269,9 @@ private static void WriteChecklist(SqliteConnection cn, SqliteTransaction tx, IR /// `state` lives in the base schema's incident_meta, so this works across schema versions; /// any failure (missing, corrupt, locked, too new) returns null so the overview degrades quietly. /// - [SuppressMessage("Design", "CA1031", +[SuppressMessage("Design", "CA1031", Justification = "Try-read: missing, corrupt, locked or too-new reads all degrade to null (see comment).")] - public IncidentState? TryReadState(string path) + public static IncidentState? TryReadState(string path) { if (!File.Exists(path)) return null; @@ -289,7 +289,7 @@ private static void WriteChecklist(SqliteConnection cn, SqliteTransaction tx, IR } } - public Incident Load(string path) + public static Incident Load(string path) { // Check before opening: SQLite would otherwise report a missing file as a bare "unable to // open database file", and the caller cannot tell that apart from a corrupt one. diff --git a/src/LageBuch.Persistence/MasterData/MasterDataStore.cs b/src/LageBuch.Persistence/MasterData/MasterDataStore.cs index 90edccd..2a598ea 100644 --- a/src/LageBuch.Persistence/MasterData/MasterDataStore.cs +++ b/src/LageBuch.Persistence/MasterData/MasterDataStore.cs @@ -11,7 +11,7 @@ public sealed class MasterDataStore /// its contents. Nothing is seeded: the app ships with no master data, so a fresh database comes /// back empty and is populated only by — i.e. the editor's Import. /// - public MasterDataSet GetOrCreate(string path) + public static MasterDataSet GetOrCreate(string path) { using var cn = SqliteConnectionFactory.OpenReadWrite(path); EnsureSchema(cn); @@ -22,7 +22,7 @@ public MasterDataSet GetOrCreate(string path) /// Replaces the master data with , in the given order. A full transactional /// replace, so deletes and reorders take effect exactly as supplied. /// - public void Save(string path, MasterDataSet set) + public static void Save(string path, MasterDataSet set) { ArgumentNullException.ThrowIfNull(set); using var cn = SqliteConnectionFactory.OpenReadWrite(path); diff --git a/tests/LageBuch.Acceptance.Tests/TestAppBuilder.cs b/tests/LageBuch.Acceptance.Tests/TestAppBuilder.cs index c5773f8..81a42b0 100644 --- a/tests/LageBuch.Acceptance.Tests/TestAppBuilder.cs +++ b/tests/LageBuch.Acceptance.Tests/TestAppBuilder.cs @@ -5,7 +5,7 @@ namespace LageBuch.Acceptance.Tests; -public static class TestAppBuilder +internal static class TestAppBuilder { // UseHeadlessDrawing = false keeps the real Skia text/render backend (bundled via // Avalonia.Desktop) so embedded custom fonts can be rasterized. The default headless diff --git a/tests/LageBuch.Documents.Tests/PdfAssert.cs b/tests/LageBuch.Documents.Tests/PdfAssert.cs index 4acf7c7..3443dfe 100644 --- a/tests/LageBuch.Documents.Tests/PdfAssert.cs +++ b/tests/LageBuch.Documents.Tests/PdfAssert.cs @@ -3,7 +3,7 @@ namespace LageBuch.Documents.Tests; -public static class PdfAssert +internal static class PdfAssert { private static readonly byte[] PdfHeader = { 0x25, 0x50, 0x44, 0x46, 0x2D }; // "%PDF-" diff --git a/tests/LageBuch.Domain.Tests/IncidentCreationTests.cs b/tests/LageBuch.Domain.Tests/IncidentCreationTests.cs index f2b5b53..3ff2a64 100644 --- a/tests/LageBuch.Domain.Tests/IncidentCreationTests.cs +++ b/tests/LageBuch.Domain.Tests/IncidentCreationTests.cs @@ -4,7 +4,7 @@ namespace LageBuch.Domain.Tests; -public sealed class FixedClock : IClock +internal sealed class FixedClock : IClock { public FixedClock(DateTimeOffset now) => Now = now; public DateTimeOffset Now { get; set; } diff --git a/tests/LageBuch.Persistence.Tests/ClosedIncidentReadOnlyTests.cs b/tests/LageBuch.Persistence.Tests/ClosedIncidentReadOnlyTests.cs index 06afcf5..cbd51bd 100644 --- a/tests/LageBuch.Persistence.Tests/ClosedIncidentReadOnlyTests.cs +++ b/tests/LageBuch.Persistence.Tests/ClosedIncidentReadOnlyTests.cs @@ -22,7 +22,7 @@ private void SaveClosedIncident() var op = new SessionOperator("Müller"); var incident = Incident.Start(clock, op); incident.Close(clock, op); - new IncidentRepository().Save(_path, incident); + IncidentRepository.Save(_path, incident); } [Fact] @@ -39,7 +39,7 @@ public void ReadOnly_connection_rejects_writes() public void Load_of_closed_incident_returns_readonly_domain_state() { SaveClosedIncident(); - var loaded = new IncidentRepository().Load(_path); + var loaded = IncidentRepository.Load(_path); Assert.Equal(IncidentState.Closed, loaded.State); Assert.Throws( () => loaded.AddForceUnit(new Clock(), new Domain.SessionOperator("Müller"), "FFB", 1)); diff --git a/tests/LageBuch.Persistence.Tests/CoMeasurementPersistenceTests.cs b/tests/LageBuch.Persistence.Tests/CoMeasurementPersistenceTests.cs index 60ec15d..2e9fa38 100644 --- a/tests/LageBuch.Persistence.Tests/CoMeasurementPersistenceTests.cs +++ b/tests/LageBuch.Persistence.Tests/CoMeasurementPersistenceTests.cs @@ -37,10 +37,10 @@ private static Incident CreateIncidentWithBuilding() public void SaveLoad_RoundTrip_BuildingsAndDwellings() { var original = CreateIncidentWithBuilding(); - var repo = new IncidentRepository(); - repo.Save(_path, original); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, original); + + var loaded = IncidentRepository.Load(_path); Assert.Single(loaded.Buildings); Assert.Equal("Haus A", loaded.Buildings[0].Name); @@ -69,10 +69,9 @@ public void SaveLoad_RoundTrip_NullableFields() var incident = Incident.Start(clock, op); incident.AddCoBuilding(clock, op, "Haus A", 1, 1); - var repo = new IncidentRepository(); - repo.Save(_path, incident); + IncidentRepository.Save(_path, incident); - var loaded = repo.Load(_path); + var loaded = IncidentRepository.Load(_path); var dwelling = loaded.Dwellings[0]; Assert.Null(dwelling.CoValue); diff --git a/tests/LageBuch.Persistence.Tests/IncidentRepositoryLoadTests.cs b/tests/LageBuch.Persistence.Tests/IncidentRepositoryLoadTests.cs index f9ab9e0..c098fbf 100644 --- a/tests/LageBuch.Persistence.Tests/IncidentRepositoryLoadTests.cs +++ b/tests/LageBuch.Persistence.Tests/IncidentRepositoryLoadTests.cs @@ -20,7 +20,7 @@ public void Loading_a_missing_file_reports_it_and_creates_nothing() { Assert.False(File.Exists(_path)); - var ex = Assert.Throws(() => new IncidentRepository().Load(_path)); + var ex = Assert.Throws(() => IncidentRepository.Load(_path)); // The path rides on FileName, not in the message: the Home banner already prefixes the // filename, so repeating it there reads as a stutter. @@ -35,7 +35,7 @@ public void Loading_a_file_that_is_not_a_database_reports_it_without_truncating_ // Picking the wrong file in the open dialog must not damage whatever was picked. File.WriteAllText(_path, "nicht wirklich eine Einsatzdatei"); - Assert.ThrowsAny(() => new IncidentRepository().Load(_path)); + Assert.ThrowsAny(() => IncidentRepository.Load(_path)); Assert.Equal("nicht wirklich eine Einsatzdatei", File.ReadAllText(_path)); diff --git a/tests/LageBuch.Persistence.Tests/IncidentRepositorySaveTests.cs b/tests/LageBuch.Persistence.Tests/IncidentRepositorySaveTests.cs index 686c79a..6f7acc2 100644 --- a/tests/LageBuch.Persistence.Tests/IncidentRepositorySaveTests.cs +++ b/tests/LageBuch.Persistence.Tests/IncidentRepositorySaveTests.cs @@ -27,7 +27,7 @@ public void Save_writes_meta_and_journal_rows() incident.SetIncidentNumber(new IncidentNumber("B 1.2 260715 4242")); incident.AddJournalEntry(clock, op, EtbDirection.Incoming, "Meldung", from: "ILS"); - new IncidentRepository().Save(_path, incident); + IncidentRepository.Save(_path, incident); using var cn = SqliteConnectionFactory.OpenReadOnly(_path); using var cmd = cn.CreateCommand(); @@ -49,7 +49,7 @@ public void Save_writes_one_etb_entry_edits_row_per_edit() incident.EditJournalEntry(clock, op, entry.Id, "Erste Korrektur"); incident.EditJournalEntry(clock, op, entry.Id, "Zweite Korrektur"); - new IncidentRepository().Save(_path, incident); + IncidentRepository.Save(_path, incident); using var cn = SqliteConnectionFactory.OpenReadOnly(_path); using var cmd = cn.CreateCommand(); @@ -62,8 +62,8 @@ public void Save_writes_one_etb_entry_edits_row_per_edit() public void Legacy_ils_number_loads_as_the_incident_number() { var clock = new Clock(); - var repo = new IncidentRepository(); - repo.Save(_path, Incident.Start(clock, new SessionOperator("Müller"))); + + IncidentRepository.Save(_path, Incident.Start(clock, new SessionOperator("Müller"))); // Simulate a file written before the unification: the number lived in ils_number and // incident_number was empty. @@ -75,7 +75,7 @@ public void Legacy_ils_number_loads_as_the_incident_number() } SqliteConnection.ClearAllPools(); - Assert.Equal("4711", repo.Load(_path).IncidentNumber!.Value); + Assert.Equal("4711", IncidentRepository.Load(_path).IncidentNumber!.Value); } [Fact] @@ -84,9 +84,9 @@ public void Save_is_idempotent_overwrite_not_append() var clock = new Clock(); var op = new SessionOperator("Müller"); var incident = Incident.Start(clock, op); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - repo.Save(_path, incident); + + IncidentRepository.Save(_path, incident); + IncidentRepository.Save(_path, incident); using var cn = SqliteConnectionFactory.OpenReadOnly(_path); using var cmd = cn.CreateCommand(); diff --git a/tests/LageBuch.Persistence.Tests/IncidentRoundTripTests.cs b/tests/LageBuch.Persistence.Tests/IncidentRoundTripTests.cs index 63e3bbd..65908f0 100644 --- a/tests/LageBuch.Persistence.Tests/IncidentRoundTripTests.cs +++ b/tests/LageBuch.Persistence.Tests/IncidentRoundTripTests.cs @@ -53,9 +53,8 @@ public void Open_incident_round_trips_all_fields() incident.AddTask(clock, op, "Nachfordern", null, TaskImportance.Low, TaskUrgency.Low, 30); incident.SetTaskCompleted(incident.Tasks[1].Id, true, clock, op); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); Assert.Equal(incident.Id, loaded.Id); Assert.Equal(IncidentState.Open, loaded.State); @@ -130,9 +129,8 @@ public void An_edited_status_and_bemerkung_survive_a_round_trip() incident.UpdateForceUnit(clock, op, unit.Id, "Im Einsatz", "Innenangriff"); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); var reloaded = Assert.Single(loaded.Forces); Assert.Equal("Im Einsatz", reloaded.Status); @@ -154,9 +152,8 @@ public void An_edited_etb_entry_and_its_history_survive_a_round_trip() var editor = new SessionOperator("Schmidt"); incident.EditJournalEntry(clock, editor, entry.Id, "Lagemeldung korrigiert"); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); var reloaded = loaded.Journal.Single(e => e.Id == entry.Id); Assert.Equal("Lagemeldung korrigiert", reloaded.Text); @@ -178,9 +175,8 @@ public void Closed_incident_round_trips_and_stays_closed() clock.Now = clock.Now.AddHours(2); incident.Close(clock, op); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); Assert.Equal(IncidentState.Closed, loaded.State); Assert.Equal(incident.ClosedAt, loaded.ClosedAt); @@ -224,9 +220,8 @@ public void Scba_trupps_round_trip_across_all_states() entryPressure: 300); var waitingId = waiting.Id; - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); Assert.Equal(4, loaded.ScbaTrupps.Count); @@ -278,9 +273,8 @@ public void Attached_files_metadata_round_trips() clock.Now = clock.Now.AddMinutes(1); incident.AddFile(clock, op, "bericht.pdf", "application/pdf", 4096); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); Assert.Equal(2, loaded.Files.Count); Assert.Equal("brand.jpg", loaded.Files[0].FileName); @@ -305,8 +299,8 @@ public void A_file_row_written_before_display_name_existed_falls_back_to_the_fil var op = new SessionOperator("Müller", "FFB 12/1"); var incident = Incident.Start(clock, op); incident.AddFile(clock, op, "brand.jpg", "image/jpeg", 2048); - var repo = new IncidentRepository(); - repo.Save(_path, incident); + + IncidentRepository.Save(_path, incident); using (var cn = SqliteConnectionFactory.OpenReadWrite(_path)) using (var cmd = cn.CreateCommand()) @@ -316,7 +310,7 @@ public void A_file_row_written_before_display_name_existed_falls_back_to_the_fil } SqliteConnection.ClearAllPools(); - var loaded = repo.Load(_path); + var loaded = IncidentRepository.Load(_path); Assert.Equal("brand.jpg", Assert.Single(loaded.Files).DisplayName); } @@ -329,9 +323,8 @@ public void Incident_timers_round_trip() var incident = Incident.Start(clock, op); incident.UpsertTimer("ils-reminder", clock.Now, intervalMinutes: 15, recurringIntervalMinutes: 30, isRunning: true); - var repo = new IncidentRepository(); - repo.Save(_path, incident); - var loaded = repo.Load(_path); + IncidentRepository.Save(_path, incident); + var loaded = IncidentRepository.Load(_path); var timer = loaded.FindTimer("ils-reminder"); Assert.NotNull(timer); @@ -352,12 +345,11 @@ public void A_removed_unit_and_its_edit_trail_do_not_survive_a_round_trip() incident.AddForceUnit(clock, op, "FFB Wache 1", 6, callSign: "FFB 1/40/1", scbaCount: 2, officerCount: 1); incident.UpdateForceStrength(clock, op, incident.Forces[0].Id, officerCount: 1, personnelCount: 9, scbaCount: 4); - var repo = new IncidentRepository(); - repo.Save(_path, incident); + IncidentRepository.Save(_path, incident); incident.RemoveForceUnit(clock, op, incident.Forces[0].Id); - repo.Save(_path, incident); + IncidentRepository.Save(_path, incident); - var loaded = repo.Load(_path); + var loaded = IncidentRepository.Load(_path); Assert.Empty(loaded.Forces); Assert.Equal((0, 0), (loaded.TotalPersonnel, loaded.TotalScba)); } @@ -371,7 +363,7 @@ public void V13_file_gains_an_empty_tasks_table_and_still_loads() var op = new SessionOperator("Müller"); var incident = Incident.Start(clock, op, "Brand"); incident.AddTask(clock, op, "Alt", null, TaskImportance.Medium, TaskUrgency.Medium, 15); - new IncidentRepository().Save(_path, incident); + IncidentRepository.Save(_path, incident); using (var cn = new SqliteConnection($"Data Source={_path}")) { @@ -382,7 +374,7 @@ public void V13_file_gains_an_empty_tasks_table_and_still_loads() } SqliteConnection.ClearAllPools(); - var loaded = new IncidentRepository().Load(_path); + var loaded = IncidentRepository.Load(_path); Assert.Empty(loaded.Tasks); // old file has no tasks — loads cleanly, migrates to V14 Assert.Equal("Brand", loaded.Keyword); diff --git a/tests/LageBuch.Persistence.Tests/IncidentStateProbeTests.cs b/tests/LageBuch.Persistence.Tests/IncidentStateProbeTests.cs index 5ef3acc..abab27b 100644 --- a/tests/LageBuch.Persistence.Tests/IncidentStateProbeTests.cs +++ b/tests/LageBuch.Persistence.Tests/IncidentStateProbeTests.cs @@ -23,34 +23,34 @@ private void Save(bool closed) var op = new SessionOperator("Müller"); var incident = Incident.Start(clock, op); if (closed) incident.Close(clock, op); - new IncidentRepository().Save(_path, incident); + IncidentRepository.Save(_path, incident); } [Fact] public void Open_incident_reads_as_Open() { Save(closed: false); - Assert.Equal(IncidentState.Open, new IncidentRepository().TryReadState(_path)); + Assert.Equal(IncidentState.Open, IncidentRepository.TryReadState(_path)); } [Fact] public void Closed_incident_reads_as_Closed() { Save(closed: true); - Assert.Equal(IncidentState.Closed, new IncidentRepository().TryReadState(_path)); + Assert.Equal(IncidentState.Closed, IncidentRepository.TryReadState(_path)); } [Fact] public void Missing_file_reads_as_null() { - Assert.Null(new IncidentRepository().TryReadState(_path)); + Assert.Null(IncidentRepository.TryReadState(_path)); } [Fact] public void Garbage_file_reads_as_null_instead_of_throwing() { File.WriteAllText(_path, "this is not a sqlite database"); - Assert.Null(new IncidentRepository().TryReadState(_path)); + Assert.Null(IncidentRepository.TryReadState(_path)); } [Fact] @@ -59,7 +59,7 @@ public void Probing_does_not_modify_the_file() Save(closed: false); var before = File.ReadAllBytes(_path); - new IncidentRepository().TryReadState(_path); + IncidentRepository.TryReadState(_path); Assert.Equal(before, File.ReadAllBytes(_path)); } diff --git a/tests/LageBuch.Persistence.Tests/MasterDataStoreTests.cs b/tests/LageBuch.Persistence.Tests/MasterDataStoreTests.cs index ba80f7b..39c94ce 100644 --- a/tests/LageBuch.Persistence.Tests/MasterDataStoreTests.cs +++ b/tests/LageBuch.Persistence.Tests/MasterDataStoreTests.cs @@ -18,22 +18,22 @@ public void GetOrCreate_returns_an_empty_set_for_a_fresh_database() { // The app ships with no seed, so a brand-new masterdata.db comes back empty -- populated // only by Save (the editor's Import), never by a compiled-in default. - var set = new MasterDataStore().GetOrCreate(_path); + var set = MasterDataStore.GetOrCreate(_path); Assert.True(set.IsEmpty); } [Fact] public void GetOrCreate_is_idempotent_and_never_seeds() { - var store = new MasterDataStore(); - Assert.True(store.GetOrCreate(_path).IsEmpty); - Assert.True(store.GetOrCreate(_path).IsEmpty); // still empty on a second open + + Assert.True(MasterDataStore.GetOrCreate(_path).IsEmpty); + Assert.True(MasterDataStore.GetOrCreate(_path).IsEmpty); // still empty on a second open } [Fact] public void Save_then_GetOrCreate_round_trips_every_category_including_personnel() { - var store = new MasterDataStore(); + var set = MasterDataSet.Empty with { Roles = new[] { "EL", "ZF" }, @@ -51,9 +51,9 @@ public void Save_then_GetOrCreate_round_trips_every_category_including_personnel Links = new[] { new Link("Wetterdienst", "https://dwd.de") }, Personnel = new[] { new Person("Mustermann", "Max", "ZF", "Land 1", "01 71 / 1 23 45 67") }, }; - store.Save(_path, set); + MasterDataStore.Save(_path, set); - var reopened = store.GetOrCreate(_path); + var reopened = MasterDataStore.GetOrCreate(_path); Assert.Equal(new[] { "EL", "ZF" }, reopened.Roles); Assert.Equal(new[] { "B", "THL" }, reopened.Einsatzarten); Assert.Equal( @@ -70,7 +70,7 @@ public void Save_then_GetOrCreate_round_trips_every_category_including_personnel [Fact] public void Vehicles_round_trip_with_wache_callsign_and_seats() { - var store = new MasterDataStore(); + var set = MasterDataSet.Empty with { Vehicles = new[] @@ -79,9 +79,9 @@ public void Vehicles_round_trip_with_wache_callsign_and_seats() new Vehicle("Aich", "Aich 42/1", 6), }, }; - store.Save(_path, set); + MasterDataStore.Save(_path, set); - var reopened = store.GetOrCreate(_path); + var reopened = MasterDataStore.GetOrCreate(_path); Assert.Equal(set.Vehicles, reopened.Vehicles); } @@ -89,13 +89,13 @@ public void Vehicles_round_trip_with_wache_callsign_and_seats() [Fact] public void Personnel_optional_fields_round_trip_as_null() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { Personnel = new[] { new Person("Musterfrau", "Erika", null, null, null) }, }); - var erika = store.GetOrCreate(_path).Personnel.Single(); + var erika = MasterDataStore.GetOrCreate(_path).Personnel.Single(); Assert.Null(erika.Role); Assert.Null(erika.CallSign); Assert.Null(erika.Phone); @@ -104,8 +104,8 @@ public void Personnel_optional_fields_round_trip_as_null() [Fact] public void Personnel_come_back_name_sorted() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { Personnel = new[] { @@ -114,24 +114,24 @@ public void Personnel_come_back_name_sorted() }, }); - var names = store.GetOrCreate(_path).Personnel.Select(p => p.LastName).ToList(); + var names = MasterDataStore.GetOrCreate(_path).Personnel.Select(p => p.LastName).ToList(); Assert.Equal(new[] { "Amsel", "Zieger" }, names); } [Fact] public void Save_round_trips_an_added_and_a_removed_street() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { Streets = new[] { new Street("Alt Str.", "FFB"), new Street("Bahnhofstr.", "FFB") }, }); - var current = store.GetOrCreate(_path); + var current = MasterDataStore.GetOrCreate(_path); var edited = current with { Streets = current.Streets.Skip(1).Append(new Street("Neu Str.", "Aich")).ToList() }; - store.Save(_path, edited); + MasterDataStore.Save(_path, edited); - var reopened = store.GetOrCreate(_path); + var reopened = MasterDataStore.GetOrCreate(_path); Assert.Contains(reopened.Streets, s => s.Name == "Neu Str." && s.District == "Aich"); Assert.DoesNotContain(reopened.Streets, s => s.Name == "Alt Str."); Assert.Equal(2, reopened.Streets.Count); @@ -140,17 +140,17 @@ public void Save_round_trips_an_added_and_a_removed_street() [Fact] public void Save_round_trips_an_added_and_a_removed_link() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { Links = new[] { new Link("Alt Link", "https://old.example"), new Link("Wetterdienst", "https://dwd.de") }, }); - var current = store.GetOrCreate(_path); + var current = MasterDataStore.GetOrCreate(_path); var edited = current with { Links = current.Links.Skip(1).Append(new Link("Neu Link", "https://new.example")).ToList() }; - store.Save(_path, edited); + MasterDataStore.Save(_path, edited); - var reopened = store.GetOrCreate(_path); + var reopened = MasterDataStore.GetOrCreate(_path); Assert.Contains(reopened.Links, l => l.Name == "Neu Link" && l.Url == "https://new.example"); Assert.DoesNotContain(reopened.Links, l => l.Name == "Alt Link"); Assert.Equal(2, reopened.Links.Count); @@ -159,15 +159,15 @@ public void Save_round_trips_an_added_and_a_removed_link() [Fact] public void Save_round_trips_a_checklist_reorder_and_delete() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { ChecklistTemplateAufbau = Items("A", "B", "C"), }); - store.Save(_path, MasterDataSet.Empty with { ChecklistTemplateAufbau = Items("C", "A") }); + MasterDataStore.Save(_path, MasterDataSet.Empty with { ChecklistTemplateAufbau = Items("C", "A") }); - Assert.Equal(Items("C", "A"), store.GetOrCreate(_path).ChecklistTemplateAufbau); + Assert.Equal(Items("C", "A"), MasterDataStore.GetOrCreate(_path).ChecklistTemplateAufbau); static IReadOnlyList Items(params string[] texts) => texts.Select(t => new ChecklistTemplateItem(t, false)).ToList(); @@ -176,14 +176,14 @@ static IReadOnlyList Items(params string[] texts) => [Fact] public void Save_round_trips_aufbau_and_abbau_independently_with_mandatory_flags() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with + + MasterDataStore.Save(_path, MasterDataSet.Empty with { ChecklistTemplateAufbau = new[] { new ChecklistTemplateItem("Fahrzeug prüfen", true) }, ChecklistTemplateAbbau = new[] { new ChecklistTemplateItem("Material zählen", false) }, }); - var reopened = store.GetOrCreate(_path); + var reopened = MasterDataStore.GetOrCreate(_path); Assert.Equal(new ChecklistTemplateItem("Fahrzeug prüfen", true), Assert.Single(reopened.ChecklistTemplateAufbau)); Assert.Equal(new ChecklistTemplateItem("Material zählen", false), Assert.Single(reopened.ChecklistTemplateAbbau)); } @@ -205,7 +205,7 @@ public void A_pre_split_database_widens_in_place_and_reads_everything_as_optiona } SqliteConnection.ClearAllPools(); - var set = new MasterDataStore().GetOrCreate(_path); + var set = MasterDataStore.GetOrCreate(_path); Assert.Equal(new ChecklistTemplateItem("Altes Item", false), Assert.Single(set.ChecklistTemplateAufbau)); Assert.Empty(set.ChecklistTemplateAbbau); @@ -214,12 +214,12 @@ public void A_pre_split_database_widens_in_place_and_reads_everything_as_optiona [Fact] public void A_value_deleted_through_Save_stays_deleted() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with { Roles = new[] { "EL", "ZF" } }); - store.Save(_path, MasterDataSet.Empty with { Roles = new[] { "ZF" } }); + MasterDataStore.Save(_path, MasterDataSet.Empty with { Roles = new[] { "EL", "ZF" } }); - Assert.DoesNotContain("EL", store.GetOrCreate(_path).Roles); + MasterDataStore.Save(_path, MasterDataSet.Empty with { Roles = new[] { "ZF" } }); + + Assert.DoesNotContain("EL", MasterDataStore.GetOrCreate(_path).Roles); } [Fact] @@ -229,24 +229,24 @@ public void A_person_without_a_first_name_displays_as_the_last_name_alone() [Fact] public void A_fresh_database_reads_the_default_settings() { - var set = new MasterDataStore().GetOrCreate(_path); + var set = MasterDataStore.GetOrCreate(_path); Assert.Equal(IncidentSettings.Defaults, set.Settings); } [Fact] public void Save_then_GetOrCreate_round_trips_settings() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with { Settings = new IncidentSettings(12, 33, 25, 18, 40, 4, 55) }); - Assert.Equal(new IncidentSettings(12, 33, 25, 18, 40, 4, 55), store.GetOrCreate(_path).Settings); + MasterDataStore.Save(_path, MasterDataSet.Empty with { Settings = new IncidentSettings(12, 33, 25, 18, 40, 4, 55) }); + + Assert.Equal(new IncidentSettings(12, 33, 25, 18, 40, 4, 55), MasterDataStore.GetOrCreate(_path).Settings); } [Fact] public void A_missing_setting_key_falls_back_to_its_default() { - var store = new MasterDataStore(); - store.Save(_path, MasterDataSet.Empty with { Settings = new IncidentSettings(12, 33, 25, 18, 40, 4, 55) }); + + MasterDataStore.Save(_path, MasterDataSet.Empty with { Settings = new IncidentSettings(12, 33, 25, 18, 40, 4, 55) }); // Simulate a store written before a setting existed: drop one row, which read must backfill. using (var cn = new SqliteConnection($"Data Source={_path}")) @@ -258,7 +258,7 @@ public void A_missing_setting_key_falls_back_to_its_default() } SqliteConnection.ClearAllPools(); - var settings = store.GetOrCreate(_path).Settings; + var settings = MasterDataStore.GetOrCreate(_path).Settings; Assert.Equal(IncidentSettings.Defaults.ReturnPressureBar, settings.ReturnPressureBar); Assert.Equal(12, settings.IlsReminderIntervalMinutes); // the other keys are untouched } diff --git a/tests/LageBuch.Persistence.Tests/MigrationForwardCompatTests.cs b/tests/LageBuch.Persistence.Tests/MigrationForwardCompatTests.cs index 8f952a8..a3967d4 100644 --- a/tests/LageBuch.Persistence.Tests/MigrationForwardCompatTests.cs +++ b/tests/LageBuch.Persistence.Tests/MigrationForwardCompatTests.cs @@ -49,8 +49,7 @@ public void Loading_a_pre_v2_incident_file_upgrades_and_does_not_crash() var incident = Domain.Incident.Start(clock, op, "Brand"); incident.AddForceUnit(clock, op, "FFB", 12); - var repo = new IncidentRepository(); - repo.Save(_path, incident); + IncidentRepository.Save(_path, incident); using (var cn = SqliteConnectionFactory.OpenReadWrite(_path)) using (var cmd = cn.CreateCommand()) @@ -63,7 +62,7 @@ public void Loading_a_pre_v2_incident_file_upgrades_and_does_not_crash() SqliteConnection.ClearAllPools(); // Act + Assert: opening the old file must upgrade it in place, not throw. - var loaded = repo.Load(_path); + var loaded = IncidentRepository.Load(_path); Assert.Equal("Brand", loaded.Keyword); Assert.Equal(12, loaded.TotalPersonnel);