Shuffle/app/test/features/auth/data/auth_status_tracker_test.dart
cschlaefke 7c2f684098
All checks were successful
CI / flutter (pull_request) Successful in 1m13s
CI / web-build (pull_request) Successful in 55s
CI / supabase (pull_request) Successful in 3s
CI / supabase-tests (pull_request) Successful in 30s
fix(auth): address review findings for password reset
Finish the recovery guard only after the password change, mark the change
in the repository so a late userUpdated event is harmless, keep the form
locked after success, carry from through the reset check-email page,
share the password error texts and focus the continue button.

Refs #1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 09:58:31 +01:00

187 lines
6.3 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shuffle/features/auth/data/auth_status_tracker.dart';
import 'package:shuffle/features/auth/domain/auth_status.dart';
import 'package:shuffle/features/auth/domain/auth_user.dart';
import 'package:supabase_flutter/supabase_flutter.dart' hide AuthUser;
import 'auth_test_support.dart';
void main() {
group('KTO-01 Kriterium 5: AuthStatusTracker event mapping', () {
test('no session -> signedOut', () {
final tracker = AuthStatusTracker();
expect(tracker.statusFor(null), const AuthStatus.signedOut());
expect(
tracker.handle(AuthChangeEvent.initialSession, null),
const AuthStatus.signedOut(),
);
});
test(
'signedIn, tokenRefreshed, userUpdated, initialSession -> signedIn',
() {
final tracker = AuthStatusTracker();
const user = AuthUser(
id: 'user-1',
email: 'anna@example.com',
emailConfirmed: true,
);
for (final event in [
AuthChangeEvent.initialSession,
AuthChangeEvent.signedIn,
AuthChangeEvent.tokenRefreshed,
AuthChangeEvent.userUpdated,
]) {
expect(
tracker.handle(event, testSession()),
const AuthStatus.signedIn(user),
reason: event.name,
);
}
},
);
test('unconfirmed email is reflected in the user', () {
final status = AuthStatusTracker().handle(
AuthChangeEvent.signedIn,
testSession(user: testUser(confirmed: false)),
);
expect((status as SignedIn).user.emailConfirmed, isFalse);
});
test('signedOut -> signedOut', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.signedIn, testSession());
expect(
tracker.handle(AuthChangeEvent.signedOut, null),
const AuthStatus.signedOut(),
);
});
});
group('KTO-01 Kriterium 8: recovery guard (process only)', () {
test('passwordRecovery -> passwordRecovery', () {
final tracker = AuthStatusTracker();
expect(
tracker.handle(AuthChangeEvent.passwordRecovery, testSession()),
isA<PasswordRecovery>(),
);
});
test('guard survives tokenRefreshed and signedIn during recovery', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession());
expect(
tracker.handle(AuthChangeEvent.tokenRefreshed, testSession()),
isA<PasswordRecovery>(),
);
expect(
tracker.handle(AuthChangeEvent.signedIn, testSession()),
isA<PasswordRecovery>(),
);
expect(tracker.statusFor(testSession()), isA<PasswordRecovery>());
});
test(
'userUpdated (password changed) keeps the guard, marks the change',
() {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession());
final status = tracker.handle(
AuthChangeEvent.userUpdated,
testSession(),
);
expect(status, isA<PasswordRecovery>());
expect((status as PasswordRecovery).passwordChanged, isTrue);
// Still held after a token refresh.
expect(
tracker.handle(AuthChangeEvent.tokenRefreshed, testSession()),
isA<PasswordRecovery>().having(
(s) => s.passwordChanged,
'changed',
true,
),
);
},
);
test('finishRecovery ends the guard', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession())
..handle(AuthChangeEvent.userUpdated, testSession());
expect(tracker.finishRecovery(testSession()), isA<SignedIn>());
expect(tracker.statusFor(testSession()), isA<SignedIn>());
});
test('finishRecovery before the password change keeps the guard', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession());
final status = tracker.finishRecovery(testSession());
expect(status, isA<PasswordRecovery>());
expect((status as PasswordRecovery).passwordChanged, isFalse);
expect(tracker.statusFor(testSession()), isA<PasswordRecovery>());
});
test('markPasswordChanged marks a running recovery (no event needed)', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession());
expect(
tracker.markPasswordChanged(testSession()),
isA<PasswordRecovery>().having(
(s) => s.passwordChanged,
'changed',
true,
),
);
// A late userUpdated is idempotent.
expect(
tracker.handle(AuthChangeEvent.userUpdated, testSession()),
isA<PasswordRecovery>().having(
(s) => s.passwordChanged,
'changed',
true,
),
);
});
test('markPasswordChanged without a recovery changes nothing', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.signedIn, testSession());
expect(tracker.markPasswordChanged(testSession()), isA<SignedIn>());
});
test('a new recovery resets the passwordChanged mark', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession())
..handle(AuthChangeEvent.userUpdated, testSession());
expect(
tracker.handle(AuthChangeEvent.passwordRecovery, testSession()),
isA<PasswordRecovery>().having(
(s) => s.passwordChanged,
'changed',
false,
),
);
});
test('userUpdated without a recovery stays signedIn', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.signedIn, testSession());
expect(
tracker.handle(AuthChangeEvent.userUpdated, testSession()),
isA<SignedIn>(),
);
});
test('signedOut (abort) ends the recovery', () {
final tracker = AuthStatusTracker()
..handle(AuthChangeEvent.passwordRecovery, testSession());
tracker.handle(AuthChangeEvent.signedOut, null);
expect(tracker.statusFor(testSession()), isA<SignedIn>());
});
test('a fresh tracker never derives recovery from a session (reload)', () {
expect(AuthStatusTracker().statusFor(testSession()), isA<SignedIn>());
});
});
}