Shuffle/app/test/features/auth/data/auth_status_tracker_test.dart
cschlaefke 87c0d7d1ec feat(auth): add password reset (forgot password, recovery link, new password)
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>
2026-11-03 09:47:03 +01:00

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>());
});
});
}