ForgotPasswordScreen with neutral reaction (F12), CheckEmail reset mode on its own path, recovery link handling and ResetPasswordScreen with the recovery guard held until the user confirms the success message. Carries the from parameter between login, register and forgot password. Removes the route placeholders. Refs #1 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
150 lines
4.9 KiB
Dart
150 lines
4.9 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('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>());
|
|
});
|
|
});
|
|
}
|