import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; 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"}'; class SocketException implements Exception {} class ClientException implements Exception {} 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(SocketException(), AuthOperation.signIn), const AuthFailure.network(), ); expect( mapAuthError(ClientException(), 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(), ); }); }); }