Shuffle/app/test/features/auth/data/auth_failure_mapper_test.dart
cschlaefke 239d0e1ac2
All checks were successful
CI / flutter (pull_request) Successful in 1m16s
CI / web-build (pull_request) Successful in 53s
CI / supabase (pull_request) Successful in 3s
CI / supabase-tests (pull_request) Successful in 32s
fix(auth): address review findings for registration
- expose legal links to screen readers on web
- allow only http(s) legal URLs, prevent double submit
- detect transport errors without runtimeType, keep extra in memory
- ship third-party license text, document decisions

Refs #1

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

266 lines
7.6 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:shuffle/features/auth/data/auth_failure_mapper.dart';
import 'package:shuffle/features/auth/domain/auth_failure.dart';
import 'package:supabase_flutter/supabase_flutter.dart' hide AuthUser;
AuthApiException _api(String code, {String status = '400'}) =>
AuthApiException('msg', statusCode: status, code: code);
const _triggerBody = '{"code":"P0001","message":"display_name_invalid"}';
void main() {
group('Spec #1 § 4.4 error mapping', () {
test('KTO-01 Kriterium 7: invalid_credentials', () {
expect(
mapAuthError(_api('invalid_credentials'), AuthOperation.signIn),
const AuthFailure.invalidCredentials(),
);
});
test('KTO-01 Kriterium 6: email_not_confirmed', () {
expect(
mapAuthError(_api('email_not_confirmed'), AuthOperation.signIn),
const AuthFailure.emailNotConfirmed(),
);
});
test('KTO-01 Kriterium 4: over_email_send_rate_limit -> emailSendLimited '
'for signUp, reset and resend', () {
for (final op in [
AuthOperation.signUp,
AuthOperation.requestPasswordReset,
AuthOperation.resend,
]) {
expect(
mapAuthError(_api('over_email_send_rate_limit', status: '429'), op),
const AuthFailure.emailSendLimited(),
reason: op.name,
);
}
});
test('over_email_send_rate_limit elsewhere is rateLimited', () {
expect(
mapAuthError(
_api('over_email_send_rate_limit', status: '429'),
AuthOperation.updatePassword,
),
const AuthFailure.rateLimited(),
);
});
test('KTO-01 Kriterium 7: over_request_rate_limit -> rateLimited', () {
expect(
mapAuthError(
_api('over_request_rate_limit', status: '429'),
AuthOperation.signIn,
),
const AuthFailure.rateLimited(),
);
});
test('any other HTTP 429 (also non-JSON body) -> rateLimited', () {
expect(
mapAuthError(
const AuthApiException('slow down', statusCode: '429'),
AuthOperation.signIn,
),
const AuthFailure.rateLimited(),
);
});
test('KTO-01 Kriterium 3: AuthWeakPasswordException with reasons', () {
expect(
mapAuthError(
AuthWeakPasswordException(
message: 'weak',
statusCode: '422',
reasons: const ['length', 'characters', 'pwned', 'other'],
),
AuthOperation.signUp,
),
const AuthFailure.weakPassword([
WeakPasswordReason.length,
WeakPasswordReason.characters,
WeakPasswordReason.pwned,
WeakPasswordReason.unknown,
]),
);
});
test('weak_password code on a plain API exception', () {
expect(
mapAuthError(
_api('weak_password', status: '422'),
AuthOperation.updatePassword,
),
const AuthFailure.weakPassword([]),
);
});
test('same_password', () {
expect(
mapAuthError(
_api('same_password', status: '422'),
AuthOperation.updatePassword,
),
const AuthFailure.samePassword(),
);
});
test('KTO-01 Kriterium 8: otp_expired -> linkInvalidOrExpired', () {
expect(
mapAuthError(
_api('otp_expired', status: '403'),
AuthOperation.verifyLink,
),
const AuthFailure.linkInvalidOrExpired(),
);
});
test('KTO-01 Kriterium 8: 403 without known code on verifyOTP', () {
expect(
mapAuthError(
const AuthApiException('invalid', statusCode: '403'),
AuthOperation.verifyLink,
),
const AuthFailure.linkInvalidOrExpired(),
);
expect(
mapAuthError(
const AuthApiException('forbidden', statusCode: '403'),
AuthOperation.signIn,
),
const AuthFailure.unexpected(),
);
});
test('email_address_invalid and validation_failed -> invalidEmail', () {
expect(
mapAuthError(_api('email_address_invalid'), AuthOperation.signUp),
const AuthFailure.invalidEmail(),
);
expect(
mapAuthError(
_api('validation_failed', status: '422'),
AuthOperation.requestPasswordReset,
),
const AuthFailure.invalidEmail(),
);
});
test('KTO-01 Kriterium 2, 10: HTTP 500 on signUp (trigger error) -> '
'registrationRejected, decided by status only', () {
final error = AuthRetryableFetchException(
message: _triggerBody,
statusCode: '500',
);
expect(error.code, isNull);
expect(
mapAuthError(error, AuthOperation.signUp),
const AuthFailure.registrationRejected(
reason: RegistrationRejectionReason.displayName,
),
);
});
test('HTTP 500 on signUp with unknown body keeps registrationRejected', () {
expect(
mapAuthError(
AuthRetryableFetchException(message: 'oops', statusCode: '500'),
AuthOperation.signUp,
),
const AuthFailure.registrationRejected(),
);
});
test('trigger messages map to reasons', () {
expect(
registrationRejectionReasonFrom('age_confirmation_required'),
RegistrationRejectionReason.ageConfirmation,
);
expect(
registrationRejectionReasonFrom('terms_acceptance_required'),
RegistrationRejectionReason.termsAcceptance,
);
expect(
registrationRejectionReasonFrom('terms_version_required'),
RegistrationRejectionReason.termsVersion,
);
});
test('HTTP 500 outside signUp is a network failure', () {
expect(
mapAuthError(
AuthRetryableFetchException(message: _triggerBody, statusCode: '500'),
AuthOperation.signIn,
),
const AuthFailure.network(),
);
});
test('other 5xx on signUp (gateway) -> network', () {
expect(
mapAuthError(
AuthRetryableFetchException(
message: 'bad gateway',
statusCode: '502',
),
AuthOperation.signUp,
),
const AuthFailure.network(),
);
});
test('Zustände Krit. 2: AuthRetryableFetchException without status', () {
expect(
mapAuthError(
AuthRetryableFetchException(message: 'Failed host lookup'),
AuthOperation.signIn,
),
const AuthFailure.network(),
);
});
test('TimeoutException and raw transport errors -> network', () {
expect(
mapAuthError(TimeoutException('t'), AuthOperation.signIn),
const AuthFailure.network(),
);
expect(
mapAuthError(const SocketException('x'), AuthOperation.signIn),
const AuthFailure.network(),
);
expect(
mapAuthError(http.ClientException('x'), AuthOperation.signIn),
const AuthFailure.network(),
);
expect(
mapAuthError(const HandshakeException('x'), AuthOperation.signIn),
const AuthFailure.network(),
);
});
test('everything else -> unexpected', () {
expect(
mapAuthError(StateError('x'), AuthOperation.signIn),
const AuthFailure.unexpected(),
);
expect(
mapAuthError(_api('something_new'), AuthOperation.signIn),
const AuthFailure.unexpected(),
);
expect(
mapAuthError(
AuthSessionMissingException(),
AuthOperation.updatePassword,
),
const AuthFailure.unexpected(),
);
});
});
}