diff --git a/lib/core/error/exceptions.dart b/lib/core/error/exceptions.dart index 451f647..664705d 100644 --- a/lib/core/error/exceptions.dart +++ b/lib/core/error/exceptions.dart @@ -1,3 +1,27 @@ +import 'package:dartz/dartz.dart'; + +import 'failures.dart'; + class ServerException implements Exception {} class CacheException implements Exception {} + +typedef Future> _function(); + +Future> catchServerException( + _function functionBody) async { + try { + return await functionBody(); + } on ServerException { + return Left(ServerFailure()); + } +} + +Future> catchCacheException( + _function functionBody) async { + try { + return await functionBody(); + } on CacheException { + return Left(CacheFailure()); + } +} diff --git a/lib/core/usecases/usecase.dart b/lib/core/usecases/usecase.dart index 94b12a0..a52017c 100644 --- a/lib/core/usecases/usecase.dart +++ b/lib/core/usecases/usecase.dart @@ -7,6 +7,8 @@ abstract class UseCase { Future> call(Params params); } +// This will be used by the code calling the use case whenever the use case +// doesn't accept any parameters. class NoParams extends Equatable { @override List get props => []; diff --git a/lib/core/util/input_converter.dart b/lib/core/util/input_converter.dart index 49c4016..600efb1 100644 --- a/lib/core/util/input_converter.dart +++ b/lib/core/util/input_converter.dart @@ -5,8 +5,12 @@ class InputConverter { Either stringToUnsignedInteger(String str) { try { final integer = int.parse(str); - if (integer < 0) throw FormatException(); + if (integer < 0) { + throw FormatException(); + } return Right(integer); + } on ArgumentError { + return Left(InvalidInputFailure()); } on FormatException { return Left(InvalidInputFailure()); } diff --git a/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart b/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart index 9a9bec4..eb094aa 100644 --- a/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart +++ b/lib/features/number_trivia/data/datasources/number_trivia_local_data_source.dart @@ -28,9 +28,8 @@ class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource { final jsonString = sharedPreferences.getString(CACHED_NUMBER_TRIVIA); if (jsonString != null) { return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString))); - } else { - throw CacheException(); } + throw CacheException(); } @override diff --git a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart index 978c225..aaa5f31 100644 --- a/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart +++ b/lib/features/number_trivia/data/datasources/number_trivia_remote_data_source.dart @@ -32,17 +32,20 @@ class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource { _getTriviaFromUrl('http://numbersapi.com/random'); Future _getTriviaFromUrl(String url) async { - final response = await client.get( - url, - headers: { - 'Content-Type': 'application/json', - }, - ); - - if (response.statusCode == 200) { - return NumberTriviaModel.fromJson(json.decode(response.body)); - } else { - throw ServerException(); + try { + final response = await client.get( + url, + headers: { + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + return NumberTriviaModel.fromJson(json.decode(response.body)); + } + } catch (e) { + print(e); } + throw ServerException(); } } diff --git a/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart b/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart index eb79106..00f2cb0 100644 --- a/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart +++ b/lib/features/number_trivia/data/repositories/number_trivia_repository_impl.dart @@ -9,7 +9,7 @@ import '../../domain/repositories/number_trivia_repository.dart'; import '../datasources/number_trivia_local_data_source.dart'; import '../datasources/number_trivia_remote_data_source.dart'; -typedef Future _ConcreteOrRandomChooser(); +typedef Future> _ConcreteOrRandomChooser(); class NumberTriviaRepositoryImpl implements NumberTriviaRepository { final NumberTriviaRemoteDataSource remoteDataSource; @@ -26,36 +26,37 @@ class NumberTriviaRepositoryImpl implements NumberTriviaRepository { Future> getConcreteNumberTrivia( int number, ) async { - return await _getTrivia(() { - return remoteDataSource.getConcreteNumberTrivia(number); + return await _handleException(() { + return saveToLocalCache(() => remoteDataSource.getConcreteNumberTrivia(number)); }); } @override Future> getRandomNumberTrivia() async { - return await _getTrivia(() { - return remoteDataSource.getRandomNumberTrivia(); + return await _handleException(() { + return saveToLocalCache(remoteDataSource.getRandomNumberTrivia); }); } - Future> _getTrivia( + Future> _handleException( _ConcreteOrRandomChooser getConcreteOrRandom, ) async { if (await networkInfo.isConnected) { - try { - final remoteTrivia = await getConcreteOrRandom(); - localDataSource.cacheNumberTrivia(remoteTrivia); - return Right(remoteTrivia); - } on ServerException { - return Left(ServerFailure()); - } - } else { - try { - final localTrivia = await localDataSource.getLastNumberTrivia(); - return Right(localTrivia); - } on CacheException { - return Left(CacheFailure()); - } + return catchServerException(getConcreteOrRandom); } + return catchCacheException(_getLastNumberTriviaInCache); + } + + Future> saveToLocalCache( + getConcreteOrRandom, + ) async { + final remoteTrivia = await getConcreteOrRandom(); + localDataSource.cacheNumberTrivia(remoteTrivia); + return Right(remoteTrivia); + } + + Future> _getLastNumberTriviaInCache() async { + final localTrivia = await localDataSource.getLastNumberTrivia(); + return Right(localTrivia); } } diff --git a/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart b/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart index 03c80ff..09dd42d 100644 --- a/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart +++ b/lib/features/number_trivia/presentation/bloc/number_trivia_bloc.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:clean_architecture_tdd_course/core/error/failures.dart'; import 'package:clean_architecture_tdd_course/core/usecases/usecase.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/entities/number_trivia.dart'; @@ -39,6 +39,8 @@ class NumberTriviaBloc extends Bloc { Stream mapEventToState( NumberTriviaEvent event, ) async* { + // Immediately branching the logic with type checking, in order + // for the event to be smart casted if (event is GetTriviaForConcreteNumber) { final inputEither = inputConverter.stringToUnsignedInteger(event.numberString); diff --git a/test/core/util/input_converter_test.dart b/test/core/util/input_converter_test.dart index 78c18e8..c0158c5 100644 --- a/test/core/util/input_converter_test.dart +++ b/test/core/util/input_converter_test.dart @@ -22,6 +22,18 @@ void main() { }, ); + test( + 'should return a Failure when the string is null', + () async { + // arrange + final str = null; + // act + final result = inputConverter.stringToUnsignedInteger(str); + // assert + expect(result, Left(InvalidInputFailure())); + }, + ); + test( 'should return a Failure when the string is not an integer', () async { diff --git a/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart b/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart index 9ad69b1..d0552c1 100644 --- a/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart +++ b/test/features/number_trivia/data/datasources/number_trivia_local_data_source_test.dart @@ -42,7 +42,7 @@ void main() { ); test( - 'should throw a CacheExeption when there is not a cached value', + 'should throw a CacheException when there is not a cached value', () async { // arrange when(mockSharedPreferences.getString(any)).thenReturn(null); diff --git a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart index 9e1e3f5..dd955a8 100644 --- a/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart +++ b/test/features/number_trivia/data/datasources/number_trivia_remote_data_source_test.dart @@ -16,6 +16,9 @@ void main() { NumberTriviaRemoteDataSourceImpl dataSource; MockHttpClient mockHttpClient; + final tNumberTriviaModel = + NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); + setUp(() { mockHttpClient = MockHttpClient(); dataSource = NumberTriviaRemoteDataSourceImpl(client: mockHttpClient); @@ -26,6 +29,11 @@ void main() { .thenAnswer((_) async => http.Response(fixture('trivia.json'), 200)); } + void setUpMockHttpClientSuccess200Infinity() { + when(mockHttpClient.get(any, headers: anyNamed('headers'))) + .thenAnswer((_) async => http.Response(fixture('trivia_infinity.json'), 200)); + } + void setUpMockHttpClientFailure404() { when(mockHttpClient.get(any, headers: anyNamed('headers'))) .thenAnswer((_) async => http.Response('Something went wrong', 404)); @@ -33,8 +41,6 @@ void main() { group('getConcreteNumberTrivia', () { final tNumber = 1; - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); test( '''should perform a GET request on a URL with number @@ -80,12 +86,9 @@ void main() { }); group('getRandomNumberTrivia', () { - final tNumberTriviaModel = - NumberTriviaModel.fromJson(json.decode(fixture('trivia.json'))); - test( - '''should perform a GET request on a URL with number - being the endpoint and with application/json header''', + '''should perform a GET request on a URL with *random* endpoint + with application/json header''', () async { // arrange setUpMockHttpClientSuccess200(); @@ -113,6 +116,19 @@ void main() { }, ); + test( + '''should throw a ServerException when the response code is 200 (success) + but the Trivia number is null (infinity)''', + () async { + // arrange + setUpMockHttpClientSuccess200Infinity(); + // act + final call = dataSource.getRandomNumberTrivia; + // assert + expect(() => call(), throwsA(TypeMatcher())); + }, + ); + test( 'should throw a ServerException when the response code is 404 or other', () async { diff --git a/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart b/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart index c4c9d61..7e90d82 100644 --- a/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart +++ b/test/features/number_trivia/data/repositories/number_trivia_repository_impl_test.dart @@ -27,6 +27,7 @@ void main() { mockRemoteDataSource = MockRemoteDataSource(); mockLocalDataSource = MockLocalDataSource(); mockNetworkInfo = MockNetworkInfo(); + repository = NumberTriviaRepositoryImpl( remoteDataSource: mockRemoteDataSource, localDataSource: mockLocalDataSource, @@ -77,7 +78,7 @@ void main() { 'should return remote data when the call to remote data source is successful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenAnswer((_) async => tNumberTriviaModel); // act final result = await repository.getConcreteNumberTrivia(tNumber); @@ -91,7 +92,7 @@ void main() { 'should cache the data locally when the call to remote data source is successful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenAnswer((_) async => tNumberTriviaModel); // act await repository.getConcreteNumberTrivia(tNumber); @@ -105,7 +106,7 @@ void main() { 'should return server failure when the call to remote data source is unsuccessful', () async { // arrange - when(mockRemoteDataSource.getConcreteNumberTrivia(any)) + when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber)) .thenThrow(ServerException()); // act final result = await repository.getConcreteNumberTrivia(tNumber); diff --git a/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart b/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart index f07116b..bdcdcf5 100644 --- a/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart +++ b/test/features/number_trivia/domain/usecases/get_random_number_trivia_test.dart @@ -1,7 +1,6 @@ import 'package:clean_architecture_tdd_course/core/usecases/usecase.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/entities/number_trivia.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/repositories/number_trivia_repository.dart'; -import 'package:clean_architecture_tdd_course/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart'; import 'package:clean_architecture_tdd_course/features/number_trivia/domain/usecases/get_random_number_trivia.dart'; import 'package:dartz/dartz.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart b/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart index 87d9089..5e9ecc9 100644 --- a/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart +++ b/test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart @@ -48,11 +48,18 @@ void main() { when(mockInputConverter.stringToUnsignedInteger(any)) .thenReturn(Right(tNumberParsed)); + void setUpMockGetConcreteNumberTriviaSuccess() => + when(mockGetConcreteNumberTrivia(any)) + .thenAnswer((_) async => Right(tNumberTrivia)); + test( 'should call the InputConverter to validate and convert the string to an unsigned integer', () async { // arrange setUpMockInputConverterSuccess(); + // MEMO: need add this, or Unhandled error NoSuchMethodError: The method 'fold' was called on null. + setUpMockGetConcreteNumberTriviaSuccess(); + // act bloc.add(GetTriviaForConcreteNumber(tNumberString)); await untilCalled(mockInputConverter.stringToUnsignedInteger(any)); @@ -83,8 +90,7 @@ void main() { () async { // arrange setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); + setUpMockGetConcreteNumberTriviaSuccess(); // act bloc.add(GetTriviaForConcreteNumber(tNumberString)); await untilCalled(mockGetConcreteNumberTrivia(any)); @@ -98,8 +104,8 @@ void main() { () async { // arrange setUpMockInputConverterSuccess(); - when(mockGetConcreteNumberTrivia(any)) - .thenAnswer((_) async => Right(tNumberTrivia)); + setUpMockGetConcreteNumberTriviaSuccess(); + // assert later final expected = [ Empty(), diff --git a/test/fixtures/trivia_infinity.json b/test/fixtures/trivia_infinity.json new file mode 100644 index 0000000..6d30fba --- /dev/null +++ b/test/fixtures/trivia_infinity.json @@ -0,0 +1,6 @@ +{ + "text": "Test Text", + "number": null, + "found": true, + "type": "trivia" +} \ No newline at end of file