Datenschutz: Fehlerberichte lassen sich abschalten
Uebertragung eines Befunds aus dem Security-Audit der alten Melo-App (21.08.2026) in die neue Codebasis. Ausgangslage hier war eine ANDERE als dort: LoggerService lud jeden Fehler unbedingt zu logs.baka-net.de hoch, es gab ueberhaupt keinen Schalter. In der alten App existierte er zwar, wirkte aber nicht ueber einen Neustart hinaus. Neu: - Schalter "Fehlerberichte senden" in den Einstellungen unter Datenschutz. - Sperre an der einzigen Stelle, durch die jeder Upload laeuft. Lokales debugPrint bleibt, aber nichts verlaesst das Geraet; der Puffer wird geleert, damit er nicht unbegrenzt waechst. - Die Entscheidung ueberlebt den Neustart: AppSettings speichert sie und reicht sie an den Logger durch, UND LoggerService liest sie beim Start zusaetzlich selbst - main() startet den Logger vor den Einstellungen, und schon dieses Zeitfenster darf bei Widerspruch nichts senden. - LoggerService bekam einen injizierbaren http.Client (Muster wie BakaAuth), damit sich belegen laesst, dass bei Opt-out KEIN Request rausgeht. Nicht uebernommen: der zweite Befund (Logout liess Zugangsdaten zurueck) trifft hier nicht zu. BakaAuth.abmelden() loescht genau die Schluessel, die anmelden() schreibt, und Navidrome hat mit loescheZugangsdaten() ein eigenes, vollstaendiges Trennen. Der alte Fehler - Logout raeumt tote Schluessel ab, waehrend die echten liegen bleiben - existiert in dieser Codebasis nicht. Standard bleibt bewusst "an", damit sich fuer bestehende Nutzer nichts ungefragt aendert. Opt-in waere datenschutzfreundlicher, ist aber eine Produktentscheidung. Tests: 304 gruen (vorher 295), 9 neue. Alle drei tragenden Stellen per Mutationsprobe geprueft. flutter analyze: keine Befunde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbuWT267W8rFDsaavANuEn
This commit is contained in:
co-authored by
Claude Opus 5
parent
2df85e650e
commit
239aae3ea3
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:melo/services/logger_service.dart';
|
||||
import 'package:melo/settings/app_settings.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Übertragung eines Befunds aus dem Security-Audit der alten Melo-App
|
||||
/// (21.08.2026): Dort stand die Diagnose-Einstellung in einer statischen
|
||||
/// Variablen mit Default `true`, die beim Start nie aus den Einstellungen
|
||||
/// gefüllt wurde — ein Opt-out überlebte keinen Neustart, während der
|
||||
/// Schalter weiterhin "aus" anzeigte.
|
||||
///
|
||||
/// Hier wird festgehalten, dass genau das nicht passieren kann.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
tearDown(() => logger.sendeDiagnosedaten = true);
|
||||
|
||||
group('AppSettings.init', () {
|
||||
test('übernimmt ein gespeichertes Opt-out und reicht es an den Logger', () async {
|
||||
SharedPreferences.setMockInitialValues({LoggerService.diagnoseKey: false});
|
||||
logger.sendeDiagnosedaten = true; // Zustand nach frischem App-Start
|
||||
|
||||
final settings = AppSettings();
|
||||
await settings.init();
|
||||
|
||||
expect(settings.sendeDiagnosedaten, isFalse);
|
||||
expect(logger.sendeDiagnosedaten, isFalse,
|
||||
reason: 'Anzeige und Sendeverhalten dürfen nicht auseinanderlaufen');
|
||||
});
|
||||
|
||||
test('ohne gespeicherte Entscheidung bleibt es beim Standard (an)', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
final settings = AppSettings();
|
||||
await settings.init();
|
||||
|
||||
expect(settings.sendeDiagnosedaten, isTrue);
|
||||
expect(logger.sendeDiagnosedaten, isTrue);
|
||||
});
|
||||
|
||||
test('ein Opt-out überlebt den Neustart', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final ersteSitzung = AppSettings();
|
||||
await ersteSitzung.init();
|
||||
await ersteSitzung.setSendeDiagnosedaten(false);
|
||||
|
||||
// Neustart: neue Instanzen, gleiche Ablage.
|
||||
logger.sendeDiagnosedaten = true;
|
||||
final zweiteSitzung = AppSettings();
|
||||
await zweiteSitzung.init();
|
||||
|
||||
expect(zweiteSitzung.sendeDiagnosedaten, isFalse);
|
||||
expect(logger.sendeDiagnosedaten, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('AppSettings.setSendeDiagnosedaten', () {
|
||||
test('schaltet den Logger sofort um und speichert', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final settings = AppSettings();
|
||||
await settings.init();
|
||||
|
||||
await settings.setSendeDiagnosedaten(false);
|
||||
expect(logger.sendeDiagnosedaten, isFalse);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getBool(LoggerService.diagnoseKey), isFalse);
|
||||
|
||||
await settings.setSendeDiagnosedaten(true);
|
||||
expect(logger.sendeDiagnosedaten, isTrue);
|
||||
expect(prefs.getBool(LoggerService.diagnoseKey), isTrue);
|
||||
});
|
||||
|
||||
test('benachrichtigt Zuhörer', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final settings = AppSettings();
|
||||
await settings.init();
|
||||
|
||||
var rufe = 0;
|
||||
settings.addListener(() => rufe++);
|
||||
await settings.setSendeDiagnosedaten(false);
|
||||
await settings.setSendeDiagnosedaten(false); // unverändert → kein Ruf
|
||||
|
||||
expect(rufe, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('LoggerService – Versand-Sperre', () {
|
||||
/// Der Kern des Opt-outs: bei Widerspruch darf KEIN Request das Gerät
|
||||
/// verlassen. Ohne diesen Test wäre genau die Sperre ungeprüft.
|
||||
test('sendet nichts, wenn der Nutzer widersprochen hat', () async {
|
||||
SharedPreferences.setMockInitialValues({LoggerService.diagnoseKey: false});
|
||||
var requests = 0;
|
||||
final dienst = LoggerService(
|
||||
client: MockClient((_) async {
|
||||
requests++;
|
||||
return http.Response('{}', 200);
|
||||
}),
|
||||
);
|
||||
await dienst.init();
|
||||
|
||||
await dienst.error('Absturz mit Details');
|
||||
await dienst.flush();
|
||||
|
||||
expect(requests, 0, reason: 'Bei Opt-out darf nichts hochgeladen werden');
|
||||
expect(dienst.logCount, 0, reason: 'Puffer darf nicht unbegrenzt wachsen');
|
||||
});
|
||||
|
||||
test('sendet, wenn der Nutzer zugestimmt hat', () async {
|
||||
SharedPreferences.setMockInitialValues({LoggerService.diagnoseKey: true});
|
||||
var requests = 0;
|
||||
final dienst = LoggerService(
|
||||
client: MockClient((_) async {
|
||||
requests++;
|
||||
return http.Response('{}', 200);
|
||||
}),
|
||||
);
|
||||
await dienst.init();
|
||||
|
||||
await dienst.error('Absturz mit Details');
|
||||
|
||||
expect(requests, greaterThan(0));
|
||||
});
|
||||
|
||||
test('das Umschalten auf aus stoppt den Versand sofort', () async {
|
||||
SharedPreferences.setMockInitialValues({LoggerService.diagnoseKey: true});
|
||||
var requests = 0;
|
||||
final dienst = LoggerService(
|
||||
client: MockClient((_) async {
|
||||
requests++;
|
||||
return http.Response('{}', 200);
|
||||
}),
|
||||
);
|
||||
await dienst.init();
|
||||
dienst.sendeDiagnosedaten = false;
|
||||
|
||||
await dienst.error('Absturz nach dem Abschalten');
|
||||
await dienst.flush();
|
||||
|
||||
expect(requests, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('LoggerService.init', () {
|
||||
test('liest das Opt-out selbst, ohne auf AppSettings zu warten', () async {
|
||||
// main() initialisiert den Logger VOR den Einstellungen — auch dieses
|
||||
// Zeitfenster darf nichts senden.
|
||||
SharedPreferences.setMockInitialValues({LoggerService.diagnoseKey: false});
|
||||
|
||||
final eigener = LoggerService();
|
||||
await eigener.init();
|
||||
|
||||
expect(eigener.sendeDiagnosedaten, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user