test: Vertragsnetz für die Storage-Fassade settings_prefs.dart

8 Tests mit dem offiziellen SharedPreferences-In-Memory-Mock: BoolPref,
StringSetPref, Theme-Prefs (Roundtrip/Teil-Save), Server-Banner inkl.
korruptes-JSON-Fehlerpfad. Sichert die Schnittstelle ab, die beim
M2-Storage-Umbau zur Pflicht-Fassade wird. Alle 22 Tests grün auf dem Pi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernd Steckmeister
2026-07-03 22:53:26 +02:00
parent e0ac5cb9fd
commit 7dfdbf1159
3 changed files with 120 additions and 4 deletions
+2
View File
@@ -1209,3 +1209,5 @@
2026-07-03 22:50 [Write] /home/steggi/pyramid/test/auth_log_test.dart
2026-07-03 22:50 [Edit] /home/steggi/pyramid/pubspec.yaml
2026-07-03 22:51 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:52 [Write] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 22:53 [Edit] /home/steggi/pyramid/PROGRESS.md
+15 -4
View File
@@ -34,16 +34,27 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt
offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
Paket. `flutter test`: **alle 14 Tests grün auf dem Pi**, `flutter analyze`
Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
Pflicht-Schnittstelle ausgebaut werden soll dieses Vertragsnetz muss beim
M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
(`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
(persistierter Wert überstimmt Default, set/toggle persistieren),
`StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
leere Map statt Crash** echter Fehlerpfad). `flutter test`: **alle
22 Tests grün auf dem Pi.**
**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) siehe
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
Pi-Testkandidaten sind erschöpft (Rest ist bibliotheksprivat oder braucht
Widget-Tests mit gemocktem Matrix-Client lohnt erst nach dem M2-Modulschnitt
mit echten öffentlichen Schnittstellen).
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
oder braucht Widget-Tests mit gemocktem Matrix-Client lohnt erst nach dem
M2-Modulschnitt mit echten öffentlichen Schnittstellen).
**Stolperfallen:** `AuthLog` cached sein File-Handle statisch der Test für
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
+103
View File
@@ -0,0 +1,103 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pyramid/core/settings_prefs.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
// Frischer In-Memory-Store pro Test (setzt auch die gecachte
// SharedPreferences-Instanz zurück).
SharedPreferences.setMockInitialValues({});
});
group('BoolPref', () {
test('lädt persistierten Wert und überstimmt damit den Default', () async {
SharedPreferences.setMockInitialValues({'notif_sound': false});
final pref = BoolPref('notif_sound', true);
expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
await pumpEventQueue();
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
});
test('set/toggle ändern Zustand und persistieren', () async {
final pref = BoolPref('notif_desktop', true);
await pumpEventQueue();
pref.toggle();
await pumpEventQueue();
expect(pref.state, false);
final p = await SharedPreferences.getInstance();
expect(p.getBool('notif_desktop'), false);
});
});
group('StringSetPref', () {
test('lädt persistierte Liste als Set, add/remove persistieren', () async {
SharedPreferences.setMockInitialValues({
'muted_rooms': ['!a:hs', '!b:hs'],
});
final pref = StringSetPref('muted_rooms');
await pumpEventQueue();
expect(pref.state, {'!a:hs', '!b:hs'});
await pref.add('!c:hs');
await pref.remove('!a:hs');
final p = await SharedPreferences.getInstance();
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
});
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
final pref = StringSetPref('muted_rooms');
await pumpEventQueue();
await pref.add('!a:hs');
await pref.add('!a:hs');
await pref.remove('!gibtsnicht:hs');
expect(pref.state, {'!a:hs'});
});
});
group('Theme-Prefs', () {
test('leerer Store liefert die dokumentierten Defaults', () async {
final t = await loadThemePrefs();
expect(t.isDark, true);
expect(t.accentIdx, 0);
expect(t.radius, 12.0);
expect(t.motion, 1.0);
});
test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
'den Rest unangetastet', () async {
await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
var t = await loadThemePrefs();
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));
// Nur ein Feld ändern die anderen dürfen nicht zurückfallen.
await saveThemePrefs(accentIdx: 1);
t = await loadThemePrefs();
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
});
});
group('Server-Banner', () {
test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
await saveServerBanner('https://hs1', 'mxc://hs1/abc');
await saveServerBanner('https://hs2', 'mxc://hs2/def');
expect(await loadAllServerBanners(),
{'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});
await saveServerBanner('https://hs1', null);
await saveServerBanner('https://hs2', '');
expect(await loadAllServerBanners(), isEmpty);
});
test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
() async {
SharedPreferences.setMockInitialValues(
{'server_banners': '{kein json'});
expect(await loadAllServerBanners(), isEmpty);
});
});
}