From 49230b1efddf5506de9835a46a9347266a56b9b1 Mon Sep 17 00:00:00 2001 From: Amaury Ricardo Date: Tue, 22 Sep 2026 16:50:09 -0300 Subject: [PATCH] fix(common): make ResultType.mapError invoke its callback mapError returned TError(e.error) without calling the callback, so AuthCubit's `..mapError((f) => isError(f))` never ran and a failed login/sign-up left the cubit in RLoading forever. The callback now runs; an Exception it returns replaces the error, anything else keeps the original. Adds the first unit tests for common (ResultType) and registers modules/common/test in sonar.tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- modules/common/lib/core/result_type.dart | 8 ++- .../common/test/core/result_type_test.dart | 61 +++++++++++++++++++ sonar-project.properties | 4 +- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 modules/common/test/core/result_type_test.dart diff --git a/modules/common/lib/core/result_type.dart b/modules/common/lib/core/result_type.dart index 77c23d2..c26ecdd 100644 --- a/modules/common/lib/core/result_type.dart +++ b/modules/common/lib/core/result_type.dart @@ -30,10 +30,16 @@ extension ResultTypeExtension on ResultType { }; } + /// Invokes [error] when this is a [TError]. If the callback returns an + /// [Exception], it replaces the original error; otherwise the original + /// error is kept, so side-effect callbacks don't discard it. ResultType mapError(Function(Exception? error) error) { return switch (this) { TSuccess e => TSuccess(e.data), - TError e => TError(e.error), + TError e => _mapErrorValue(error(e.error), e.error), }; } + + TError _mapErrorValue(dynamic mapped, Exception? original) => + TError(mapped is Exception ? mapped : original); } diff --git a/modules/common/test/core/result_type_test.dart b/modules/common/test/core/result_type_test.dart new file mode 100644 index 0000000..daa644f --- /dev/null +++ b/modules/common/test/core/result_type_test.dart @@ -0,0 +1,61 @@ +import 'package:common/core/failure/failure.dart'; +import 'package:common/core/result_type.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ResultType.mapError', () { + test('invokes the callback with the error of a TError', () { + final failure = UnexpectedFailure('boom'); + Exception? received; + + TError(failure).mapError((error) => received = error); + + expect(received, same(failure)); + }); + + test('keeps the original error when the callback returns no Exception', + () { + final failure = UnexpectedFailure('boom'); + + final result = TError(failure).mapError((_) {}); + + expect(result, isA>()); + expect((result as TError).error, same(failure)); + }); + + test('replaces the error when the callback returns an Exception', () { + final mapped = ConnectionFailure('offline'); + + final result = TError(UnexpectedFailure()).mapError((_) => mapped); + + expect((result as TError).error, same(mapped)); + }); + + test('does not invoke the callback for a TSuccess', () { + var called = false; + + final result = TSuccess(1).mapError((_) => called = true); + + expect(called, isFalse); + expect((result as TSuccess).data, 1); + }); + }); + + group('ResultType.mapSuccess', () { + test('invokes the callback with the data of a TSuccess', () { + int? received; + + TSuccess(42).mapSuccess((data) => received = data); + + expect(received, 42); + }); + + test('does not invoke the callback for a TError', () { + var called = false; + + TError(UnexpectedFailure()).mapSuccess((_) => called = true); + + expect(called, isFalse); + }); + }); +} diff --git a/sonar-project.properties b/sonar-project.properties index 6d1cbdd..7e07bdc 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,8 +11,8 @@ sonar.dart.analyzer.report.mode=LEGACY sonar.exclusions=**/*.g.dart,**/generated/**,**/*.freezed.dart, pubspec.yaml, coverage/** sonar.coverage.exclusions=**/*.g.dart,**/generated/**,**/*.freezed.dart, pubspec.yaml, coverage/** -# common & data have no tests yet -sonar.tests=app/test,modules/domain/test +# domain & data have no tests yet +sonar.tests=app/test,modules/domain/test,modules/common/test # Coverage report – property understood by the **sonar-flutter** plugin sonar.flutter.coverage.reportPath=coverage/lcov.info