diff --git a/catalyst_voices/packages/catalyst_voices_repositories/README.md b/catalyst_voices/packages/catalyst_voices_repositories/README.md index 9ff8a84614a..5316d7d4865 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/README.md +++ b/catalyst_voices/packages/catalyst_voices_repositories/README.md @@ -1,23 +1 @@ # Catalyst Voices Repositories - -## Catalyst Data Gateway Repository - -### Tests - -When extending the `CatalystDataGatewayRepository` it is necessary to generate -proper mocks to have them available in tests. -To do that we need to run - -```sh -flutter pub run build_runner build --delete-conflicting-outputs -``` - - or - - ```sh -dart run build_runner build --delete-conflicting-outputs -``` - -in the Catalyst Voices Repositories package root. -The decorator `@GenerateNiceMocks` provided by mockito is used to indicate the -repository to generate the mocks for. diff --git a/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart index 7dafda84579..083c0ba800c 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart +++ b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart @@ -6,11 +6,53 @@ import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_ import 'package:chopper/chopper.dart'; import 'package:result_type/result_type.dart'; -interface class CatalystDataGatewayRepository { +// The [CatalystDataGatewayRepository] provides a structured interface to +// interact with the `catalyst-gateway` backend API. +// Network communication and error handling is abstracted allowing the +// integration of API calls in an easy way. +// All methods return `Future` objects to allow async execution. +// +// The repository uses, under the hood, the [CatGatewayApi] directly generated +// from backend OpenAPI specification. +// +// To use the repository is necessary to initialize it by specifying the API +// base URL: +// +// ```dart +// final repository = CatalystDataGatewayRepository(Uri.parse('https://example.org/api')); +// ``` +// +// Once initialized it is possible, for example, to check the health status of +// the service: +// +// ```dart +// final health_status = await repository.getHealthLive(); +// ``` +// +// fetch staked ADA by stake address: +// +// ```dart +// final stake_info = await repository.getCardanoStakedAdaStakeAddress( +// // cspell: disable-next-line +// stakeAddress:'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw', +// ); +// ``` +// +// or get the sync state: +// +// ```dart +// final sync_state = await repository.getCardanoSyncState(); +// ``` + +final class CatalystDataGatewayRepository { final CatGatewayApi _catGatewayApi; - CatalystDataGatewayRepository(Uri baseUrl) - : _catGatewayApi = CatGatewayApi.create(baseUrl: baseUrl); + CatalystDataGatewayRepository( + Uri baseUrl, + {CatGatewayApi? catGatewayApiInstance,} + ) + : _catGatewayApi = catGatewayApiInstance ?? + CatGatewayApi.create(baseUrl: baseUrl); Future> getHealthStarted() async { try { @@ -42,13 +84,13 @@ interface class CatalystDataGatewayRepository { Future> getCardanoStakedAdaStakeAddress({ required String stakeAddress, enums.Network network = enums.Network.mainnet, - DateTime? dateTime, + int? slotNumber, }) async { try { final stakeInfo = await _catGatewayApi.apiCardanoStakedAdaStakeAddressGet( stakeAddress: stakeAddress, network: network, - dateTime: dateTime, + slotNumber: slotNumber, ); return Success(stakeInfo.bodyOrThrow); } on ChopperHttpException catch (error) { diff --git a/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml index a2453a86a7f..83d79fee1a6 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: chopper: ^7.2.0 flutter: sdk: flutter + http: ^1.2.1 result_type: ^0.2.0 rxdart: ^0.27.7 diff --git a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart index faa941857e9..95cf84e714b 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart +++ b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart @@ -1,145 +1,198 @@ +import 'dart:io'; + import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/catalyst_data_gateway_repository.dart'; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.enums.swagger.dart' + as enums; import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.swagger.dart'; -import 'package:mockito/annotations.dart'; +import 'package:chopper/chopper.dart' as chopper; +import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; -import 'package:result_type/result_type.dart'; import 'package:test/test.dart'; -import 'catalyst_data_gateway_repository_test.mocks.dart'; +// `@GenerateNiceMocks` from `mockito` can't be used here because +// `chopper.Response` use the `base` modifier which "disallows +// implementations outside of its own library". For this reason +// `@GenerateNiceMocks` doesn't work as intended and we opted to +// mock the `CatGatewayApi` using `Fake`. +class FakeCatGatewayApi extends Fake implements CatGatewayApi { + final chopper.Response response; + + FakeCatGatewayApi(this.response); + + @override + Future> apiHealthStartedGet() async => response; + @override + Future> apiHealthReadyGet() async => response; + + @override + Future> apiHealthLiveGet() async => response; + + @override + Future> apiCardanoStakedAdaStakeAddressGet({ + required String? stakeAddress, + enums.Network? network, + int? slotNumber, + }) async => + response as chopper.Response; + + @override + Future> apiCardanoSyncStateGet({ + enums.Network? network, + }) async => + response as chopper.Response; +} -@GenerateNiceMocks([MockSpec()]) void main() { + CatalystDataGatewayRepository setupRepository( + chopper.Response response, + ) { + final fakeCatGatewayApi = FakeCatGatewayApi(response); + return CatalystDataGatewayRepository( + Uri.parse('https://localhost/api'), + catGatewayApiInstance: fakeCatGatewayApi, + ); + } + group('CatalystDataGatewayRepository', () { - final mock = MockCatalystDataGatewayRepository(); + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.noContent), null), + ); test('getHealthStarted success', () async { - when(mock.getHealthStarted()).thenAnswer((_) async => Success(null)); - final result = await mock.getHealthStarted(); + final result = await repository.getHealthStarted(); expect(result.isSuccess, true); }); test('getHealthStarted Internal Server Error', () async { - when(mock.getHealthStarted()).thenAnswer((_) async { - return Failure(NetworkErrors.internalServerError); - }); - - final result = await mock.getHealthStarted(); - + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.internalServerError), + null, + ), + ); + final result = await repository.getHealthStarted(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.internalServerError)); }); test('getHealthStarted Service Unavailable', () async { - when(mock.getHealthStarted()).thenAnswer((_) async { - return Failure(NetworkErrors.serviceUnavailable); - }); - final result = await mock.getHealthStarted(); + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.serviceUnavailable), + null, + ), + ); + final result = await repository.getHealthStarted(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.serviceUnavailable)); }); test('getHealthReady success', () async { - when(mock.getHealthReady()).thenAnswer((_) async => Success(null)); - final result = await mock.getHealthReady(); + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.noContent), null), + ); + final result = await repository.getHealthReady(); expect(result.isSuccess, true); }); + test('getHealthReady Internal Server Error', () async { - when(mock.getHealthReady()).thenAnswer((_) async { - return Failure(NetworkErrors.internalServerError); - }); - final result = await mock.getHealthReady(); + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.internalServerError), + null, + ), + ); + final result = await repository.getHealthReady(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.internalServerError)); }); test('getHealthReady Service Unavailable', () async { - when(mock.getHealthReady()).thenAnswer((_) async { - return Failure(NetworkErrors.serviceUnavailable); - }); - final result = await mock.getHealthReady(); + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.serviceUnavailable), + null, + ), + ); + final result = await repository.getHealthReady(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.serviceUnavailable)); }); test('getHealthLive success', () async { - when(mock.getHealthLive()).thenAnswer((_) async => Success(null)); - final result = await mock.getHealthLive(); + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.ok), null), + ); + final result = await repository.getHealthLive(); expect(result.isSuccess, true); }); test('getHealthLive Internal Server Error', () async { - when(mock.getHealthLive()).thenAnswer((_) async { - return Failure(NetworkErrors.internalServerError); - }); - final result = await mock.getHealthLive(); + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.internalServerError), + null, + ), + ); + final result = await repository.getHealthLive(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.internalServerError)); }); test('getHealthLive Service Unavailable', () async { - when(mock.getHealthLive()).thenAnswer((_) async { - return Failure(NetworkErrors.serviceUnavailable); - }); - final result = await mock.getHealthLive(); + final repository = setupRepository( + chopper.Response(http.Response('', 503), null), + ); + final result = await repository.getHealthLive(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.serviceUnavailable)); }); // cspell: disable - const validStakeAddress = - 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw'; + const validStakeAddress = + 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw'; // cspell: enable const notValidStakeAddress = 'stake1wrong1stake'; + test('getCardanoStakedAdaStakeAddress success', () async { - final stakeInfo = StakeInfo( + const stakeInfo = StakeInfo( amount: 1, slotNumber: 5, - blockTime: DateTime.utc(1970), ); - when( - mock.getCardanoStakedAdaStakeAddress( - stakeAddress: validStakeAddress, - ), - ).thenAnswer((_) async => Success(stakeInfo)); - final result = await mock.getCardanoStakedAdaStakeAddress( + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.ok), stakeInfo), + ); + final result = await repository.getCardanoStakedAdaStakeAddress( stakeAddress: validStakeAddress, ); expect(result.isSuccess, true); expect(result.success, equals(stakeInfo)); }); test('getCardanoStakedAdaStakeAddress Bad request', () async { - when( - mock.getCardanoStakedAdaStakeAddress( - stakeAddress: notValidStakeAddress, - ), - ).thenAnswer((_) async { - return Failure(NetworkErrors.badRequest); - }); - final result = await mock.getCardanoStakedAdaStakeAddress( + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.badRequest), null), + ); + final result = await repository.getCardanoStakedAdaStakeAddress( stakeAddress: notValidStakeAddress, ); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.badRequest)); }); + test('getCardanoStakedAdaStakeAddress Not found', () async { - when( - mock.getCardanoStakedAdaStakeAddress( - stakeAddress: validStakeAddress, - ), - ).thenAnswer((_) async { - return Failure(NetworkErrors.notFound); - }); - final result = await mock.getCardanoStakedAdaStakeAddress( - stakeAddress: validStakeAddress, + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.notFound), null), + ); + final result = await repository.getCardanoStakedAdaStakeAddress( + stakeAddress: notValidStakeAddress, ); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.notFound)); }); test('getCardanoStakedAdaStakeAddress Server Error', () async { - when( - mock.getCardanoStakedAdaStakeAddress( - stakeAddress: validStakeAddress, + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.internalServerError), + null, ), - ).thenAnswer((_) async { - return Failure(NetworkErrors.internalServerError); - }); - final result = await mock.getCardanoStakedAdaStakeAddress( + ); + final result = await repository.getCardanoStakedAdaStakeAddress( stakeAddress: validStakeAddress, ); expect(result.isFailure, true); @@ -147,47 +200,55 @@ void main() { }); test('getCardanoStakedAdaStakeAddress Service Unavailable', () async { - when(mock.getCardanoStakedAdaStakeAddress( - stakeAddress: validStakeAddress, - ),).thenAnswer((_) async { - return Failure(NetworkErrors.serviceUnavailable); - }); - final result = await mock.getCardanoStakedAdaStakeAddress( + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.serviceUnavailable), + null, + ), + ); + final result = await repository.getCardanoStakedAdaStakeAddress( stakeAddress: validStakeAddress, ); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.serviceUnavailable)); }); + test('getCardanoSyncState success', () async { final syncState = SyncState( slotNumber: 5, - blockHash: + blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', lastUpdated: DateTime.utc(1970), ); - when(mock.getCardanoSyncState()).thenAnswer( - (_) async => Success(syncState), + final repository = setupRepository( + chopper.Response(http.Response('', HttpStatus.ok), syncState), ); - final result = await mock.getCardanoSyncState(); + final result = await repository.getCardanoSyncState(); expect(result.isSuccess, true); expect(result.success, equals(syncState)); }); + test('getCardanoSyncState Server Error', () async { - when(mock.getCardanoSyncState()).thenAnswer( - (_) async => Failure(NetworkErrors.internalServerError), + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.internalServerError), + null, + ), ); - final result = await mock.getCardanoSyncState(); + final result = await repository.getCardanoSyncState(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.internalServerError)); }); test('getCardanoSyncState Service Unavailable', () async { - when(mock.getCardanoSyncState()).thenAnswer( - (_) async => Failure(NetworkErrors.serviceUnavailable), + final repository = setupRepository( + chopper.Response( + http.Response('', HttpStatus.serviceUnavailable), + null, + ), ); - final result = await mock.getCardanoSyncState(); + final result = await repository.getCardanoSyncState(); expect(result.isFailure, true); expect(result.failure, equals(NetworkErrors.serviceUnavailable)); }); - }); } diff --git a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart deleted file mode 100644 index b64461a2ded..00000000000 --- a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart +++ /dev/null @@ -1,201 +0,0 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in catalyst_voices_repositories/test/src/catalyst_data_gateway_repository_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart' as _i5; -import 'package:catalyst_voices_repositories/src/catalyst_data_gateway_repository.dart' - as _i3; -import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.enums.swagger.dart' - as _i7; -import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.swagger.dart' - as _i6; -import 'package:mockito/mockito.dart' as _i1; -import 'package:result_type/result_type.dart' as _i2; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeResult_0 extends _i1.SmartFake implements _i2.Result { - _FakeResult_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [CatalystDataGatewayRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockCatalystDataGatewayRepository extends _i1.Mock - implements _i3.CatalystDataGatewayRepository { - @override - _i4.Future<_i2.Result> getHealthStarted() => - (super.noSuchMethod( - Invocation.method( - #getHealthStarted, - [], - ), - returnValue: _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthStarted, - [], - ), - )), - returnValueForMissingStub: - _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthStarted, - [], - ), - )), - ) as _i4.Future<_i2.Result>); - - @override - _i4.Future<_i2.Result> getHealthReady() => - (super.noSuchMethod( - Invocation.method( - #getHealthReady, - [], - ), - returnValue: _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthReady, - [], - ), - )), - returnValueForMissingStub: - _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthReady, - [], - ), - )), - ) as _i4.Future<_i2.Result>); - - @override - _i4.Future<_i2.Result> getHealthLive() => - (super.noSuchMethod( - Invocation.method( - #getHealthLive, - [], - ), - returnValue: _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthLive, - [], - ), - )), - returnValueForMissingStub: - _i4.Future<_i2.Result>.value( - _FakeResult_0( - this, - Invocation.method( - #getHealthLive, - [], - ), - )), - ) as _i4.Future<_i2.Result>); - - @override - _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>> - getCardanoStakedAdaStakeAddress({ - required String? stakeAddress, - _i7.Network? network = _i7.Network.mainnet, - DateTime? dateTime, - }) => - (super.noSuchMethod( - Invocation.method( - #getCardanoStakedAdaStakeAddress, - [], - { - #stakeAddress: stakeAddress, - #network: network, - #dateTime: dateTime, - }, - ), - returnValue: - _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>.value( - _FakeResult_0<_i6.StakeInfo, _i5.NetworkErrors>( - this, - Invocation.method( - #getCardanoStakedAdaStakeAddress, - [], - { - #stakeAddress: stakeAddress, - #network: network, - #dateTime: dateTime, - }, - ), - )), - returnValueForMissingStub: - _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>.value( - _FakeResult_0<_i6.StakeInfo, _i5.NetworkErrors>( - this, - Invocation.method( - #getCardanoStakedAdaStakeAddress, - [], - { - #stakeAddress: stakeAddress, - #network: network, - #dateTime: dateTime, - }, - ), - )), - ) as _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>); - - @override - _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>> getCardanoSyncState( - {_i7.Network? network = _i7.Network.mainnet}) => - (super.noSuchMethod( - Invocation.method( - #getCardanoSyncState, - [], - {#network: network}, - ), - returnValue: - _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>.value( - _FakeResult_0<_i6.SyncState, _i5.NetworkErrors>( - this, - Invocation.method( - #getCardanoSyncState, - [], - {#network: network}, - ), - )), - returnValueForMissingStub: - _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>.value( - _FakeResult_0<_i6.SyncState, _i5.NetworkErrors>( - this, - Invocation.method( - #getCardanoSyncState, - [], - {#network: network}, - ), - )), - ) as _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>); -}