300 lines
8.5 KiB
Dart
300 lines
8.5 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('F13: HTTP 500 on signUp with unknown body (SMTP) -> unexpected', () {
|
|
expect(
|
|
mapAuthError(
|
|
AuthRetryableFetchException(
|
|
message: '{"code":"unexpected_failure","msg":"Error sending"}',
|
|
statusCode: '500',
|
|
),
|
|
AuthOperation.signUp,
|
|
),
|
|
const AuthFailure.unexpected(),
|
|
);
|
|
});
|
|
|
|
test('F13: HTTP 5xx on reset and resend -> unexpected, not network', () {
|
|
for (final op in [
|
|
AuthOperation.requestPasswordReset,
|
|
AuthOperation.resend,
|
|
]) {
|
|
for (final status in ['500', '502', '504']) {
|
|
expect(
|
|
mapAuthError(
|
|
AuthRetryableFetchException(message: 'x', statusCode: status),
|
|
op,
|
|
),
|
|
const AuthFailure.unexpected(),
|
|
reason: '$op $status',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('F13: a transport error without status stays network', () {
|
|
for (final op in [
|
|
AuthOperation.signUp,
|
|
AuthOperation.requestPasswordReset,
|
|
AuthOperation.resend,
|
|
]) {
|
|
expect(
|
|
mapAuthError(AuthRetryableFetchException(message: 'x'), op),
|
|
const AuthFailure.network(),
|
|
);
|
|
}
|
|
});
|
|
|
|
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('F13: other 5xx on signUp (gateway) -> unexpected (neutral)', () {
|
|
expect(
|
|
mapAuthError(
|
|
AuthRetryableFetchException(
|
|
message: 'bad gateway',
|
|
statusCode: '502',
|
|
),
|
|
AuthOperation.signUp,
|
|
),
|
|
const AuthFailure.unexpected(),
|
|
);
|
|
});
|
|
|
|
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(),
|
|
);
|
|
});
|
|
});
|
|
}
|