Auto-Anmeldung: doppelten Login-Request beim App-Start verhindert (Race durch IndexedStack)
Betraf alle drei Nutzer (Dustin, Baka, Tinker), nicht nur einen: IndexedStack baut Download-Tab und YT-Suche beim App-Start gleichzeitig, beide loesen unabhaengig voneinander autoAnmelden() aus. Ohne In-Flight-Absicherung gingen zwei fast-gleichzeitige Login-Requests an denselben Server, von denen einer abgelehnt wurde - obwohl die Zugangsdaten korrekt waren. - BakaAuth.autoAnmelden() haengt einen ueberlappenden Aufruf jetzt an den bereits laufenden Login-Versuch an, statt einen zweiten Request zu senden. - Anmelde-Dialog im Download-Tab zeigt den tatsaechlich bekannten Navidrome-Benutzernamen statt immer "Baka" vorauszufuellen. - Navidrome-Dialog in den Einstellungen waehlt den zuletzt bekannten Benutzer vor statt immer "Baka".
This commit is contained in:
@@ -679,9 +679,10 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _anmelden() async {
|
Future<void> _anmelden() async {
|
||||||
|
final nav = context.read<DownloadService>().navidrome;
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => const _AnmeldeDialog(),
|
builder: (_) => _AnmeldeDialog(benutzer: nav.benutzer),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,14 +870,18 @@ class _YouTubeBereichState extends State<_YouTubeBereich> {
|
|||||||
|
|
||||||
/// Fragt Benutzername und Passwort ab und gibt beides zurück.
|
/// Fragt Benutzername und Passwort ab und gibt beides zurück.
|
||||||
class _AnmeldeDialog extends StatefulWidget {
|
class _AnmeldeDialog extends StatefulWidget {
|
||||||
const _AnmeldeDialog();
|
const _AnmeldeDialog({this.benutzer = ''});
|
||||||
|
|
||||||
|
/// Bereits bekannter Navidrome-Benutzername (leer, falls keiner bekannt) —
|
||||||
|
/// vorausgefüllt statt immer "Baka" zu zeigen, egal wer die App nutzt.
|
||||||
|
final String benutzer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AnmeldeDialog> createState() => _AnmeldeDialogState();
|
State<_AnmeldeDialog> createState() => _AnmeldeDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AnmeldeDialogState extends State<_AnmeldeDialog> {
|
class _AnmeldeDialogState extends State<_AnmeldeDialog> {
|
||||||
final _benutzer = TextEditingController(text: 'Baka');
|
late final _benutzer = TextEditingController(text: widget.benutzer);
|
||||||
final _passwort = TextEditingController();
|
final _passwort = TextEditingController();
|
||||||
bool _sichtbar = false;
|
bool _sichtbar = false;
|
||||||
bool _verbindet = false;
|
bool _verbindet = false;
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ class BakaAuth extends ChangeNotifier {
|
|||||||
String? _token;
|
String? _token;
|
||||||
String _benutzer = '';
|
String _benutzer = '';
|
||||||
|
|
||||||
|
/// Laufender Login-Versuch aus [autoAnmelden] — solange er offen ist,
|
||||||
|
/// hängt sich ein überlappender Aufruf daran statt einen zweiten
|
||||||
|
/// Request loszuschicken (Race Condition: mehrere Tabs im IndexedStack
|
||||||
|
/// lösen beim App-Start gleichzeitig autoAnmelden aus).
|
||||||
|
Future<void>? _laufenderAutoLogin;
|
||||||
|
|
||||||
bool get istAngemeldet => _token != null && _token!.isNotEmpty;
|
bool get istAngemeldet => _token != null && _token!.isNotEmpty;
|
||||||
String get benutzer => _benutzer;
|
String get benutzer => _benutzer;
|
||||||
|
|
||||||
@@ -108,7 +114,9 @@ class BakaAuth extends ChangeNotifier {
|
|||||||
/// authentifizierten Aufruf ohnehin automatisch.
|
/// authentifizierten Aufruf ohnehin automatisch.
|
||||||
Future<void> autoAnmelden(String benutzer, String passwort) async {
|
Future<void> autoAnmelden(String benutzer, String passwort) async {
|
||||||
if (istAngemeldet || benutzer.isEmpty || passwort.isEmpty) return;
|
if (istAngemeldet || benutzer.isEmpty || passwort.isEmpty) return;
|
||||||
await anmelden(benutzer, passwort);
|
_laufenderAutoLogin ??= anmelden(benutzer, passwort)
|
||||||
|
.whenComplete(() => _laufenderAutoLogin = null);
|
||||||
|
await _laufenderAutoLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> abmelden() async {
|
Future<void> abmelden() async {
|
||||||
|
|||||||
@@ -346,7 +346,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
void _zeigeNavidromeDialog() {
|
void _zeigeNavidromeDialog() {
|
||||||
final urlCtrl = TextEditingController(text: 'https://musik.baka-net.de');
|
final urlCtrl = TextEditingController(text: 'https://musik.baka-net.de');
|
||||||
final passCtrl = TextEditingController();
|
final passCtrl = TextEditingController();
|
||||||
String selectedUser = 'Baka';
|
// Vorauswahl: der zuletzt bekannte Navidrome-Benutzer, falls er einer
|
||||||
|
// der drei Server-User ist — sonst Fallback "Baka".
|
||||||
|
const bekannteBenutzer = ['Baka', 'Tinker', 'Dustin'];
|
||||||
|
String selectedUser = bekannteBenutzer.contains(_navidrome.benutzer)
|
||||||
|
? _navidrome.benutzer
|
||||||
|
: 'Baka';
|
||||||
bool verbindet = false;
|
bool verbindet = false;
|
||||||
bool obscure = true;
|
bool obscure = true;
|
||||||
String? fehler;
|
String? fehler;
|
||||||
|
|||||||
@@ -248,6 +248,26 @@ void main() {
|
|||||||
findsOneWidget);
|
findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Der Anmelde-Dialog schlägt den bekannten Navidrome-Benutzer vor, '
|
||||||
|
'nicht immer "Baka"', (tester) async {
|
||||||
|
final auth = BakaAuth(
|
||||||
|
client: MockClient((_) async => http.Response('', 500)),
|
||||||
|
speicher: _MemorySpeicher());
|
||||||
|
final nav = NavidromeService()
|
||||||
|
..setCredentials('https://musik.baka-net.de', 'Tinker', 'falsch');
|
||||||
|
|
||||||
|
await tester.pumpWidget(_wrap(auth,
|
||||||
|
db: db, categories: categories, navidrome: nav));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Beim Baka-Konto anmelden'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final feld = tester.widget<TextField>(find.byType(TextField).first);
|
||||||
|
expect(feld.controller!.text, 'Tinker');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Server-User meldet sich im Hintergrund mit denselben Zugangsdaten '
|
'Server-User meldet sich im Hintergrund mit denselben Zugangsdaten '
|
||||||
'beim Baka-Konto an', (tester) async {
|
'beim Baka-Konto an', (tester) async {
|
||||||
|
|||||||
@@ -115,6 +115,30 @@ void main() {
|
|||||||
expect(auth.benutzer, 'Baka');
|
expect(auth.benutzer, 'Baka');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'autoAnmelden löst bei gleichzeitigen Aufrufen nur EINEN HTTP-Request aus '
|
||||||
|
'(Race Condition durch IndexedStack, das mehrere Tabs gleichzeitig baut)',
|
||||||
|
() async {
|
||||||
|
var aufrufe = 0;
|
||||||
|
final client = MockClient((_) async {
|
||||||
|
aufrufe++;
|
||||||
|
// Künstliche Verzögerung: simuliert, dass der erste Request noch
|
||||||
|
// nicht zurück ist, wenn der zweite Aufruf startet.
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
return http.Response(jsonEncode({'token': 'jwt-tinker'}), 200);
|
||||||
|
});
|
||||||
|
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
auth.autoAnmelden('Tinker', 'geheim'),
|
||||||
|
auth.autoAnmelden('Tinker', 'geheim'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(aufrufe, 1);
|
||||||
|
expect(auth.istAngemeldet, isTrue);
|
||||||
|
expect(auth.benutzer, 'Tinker');
|
||||||
|
});
|
||||||
|
|
||||||
test('autoAnmelden tut nichts ohne Zugangsdaten', () async {
|
test('autoAnmelden tut nichts ohne Zugangsdaten', () async {
|
||||||
var aufrufe = 0;
|
var aufrufe = 0;
|
||||||
final client = MockClient((_) async {
|
final client = MockClient((_) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user