diff --git a/.gitignore b/.gitignore index b4a256a..88264d9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,6 @@ # Web related lib/generated_plugin_registrant.dart - +coverage/ # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/.travis.yml b/.travis.yml index 2959247..1564f61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,9 @@ before_script: - chmod +x generate-apks.sh script: - ./flutter/bin/flutter build apk + - ./flutter/bin/flutter test --coverage + - bash <(curl -s https://codecov.io/bash) + after_success: - ./generate-apks.sh cache: diff --git a/README.md b/README.md index e891713..7f617b8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Cross-platform client for Mentorship System -| Branch | [Travis](https://travis-ci.org/) | -| :----------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------: | -| [develop](https://github.com/anitab-org/mentorship-flutter/tree/develop) | [![Build Status](https://travis-ci.com/anitab-org/mentorship-flutter.svg?branch=develop)](https://travis-ci.com/anitab-org/mentorship-flutter) | + +| Branch | [Travis](https://travis-ci.org/) | Codecov | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| [develop](https://github.com/anitab-org/mentorship-flutter/tree/develop) | [![Build Status](https://travis-ci.com/anitab-org/mentorship-flutter.svg?branch=develop)](https://travis-ci.com/anitab-org/mentorship-flutter) | [![codecov](https://codecov.io/gh/anitab-org/mentorship-flutter/branch/develop/graph/badge.svg)](https://codecov.io/gh/anitab-org/mentorship-flutter) | + Mentorship System is an application that allows women in tech to mentor each other, on career development topics, through 1:1 relations for a certain period of time. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..accceea --- /dev/null +++ b/codecov.yml @@ -0,0 +1,12 @@ +comment: + layout: "diff, files" + behavior: new + require_changes: false # if true: only post the comment if coverage changes + +coverage: + status: + project: + default: + target: auto # will use the coverage from the base commit + threshold: 0% + base: auto diff --git a/lib/screens/login/bloc/login_state.dart b/lib/screens/login/bloc/login_state.dart index a8539c8..33035e6 100644 --- a/lib/screens/login/bloc/login_state.dart +++ b/lib/screens/login/bloc/login_state.dart @@ -20,4 +20,6 @@ class LoginFailure extends LoginState { @override List get props => [message]; + @override + String toString() => 'LoginFailure { error: $message }'; } diff --git a/pubspec.lock b/pubspec.lock index dd4a0fe..6b3b4ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "7.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.15" + version: "0.39.17" archive: dependency: transitive description: @@ -35,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.5.0-nullsafety" auto_size_text: dependency: "direct main" description: @@ -49,14 +49,21 @@ packages: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "6.0.3" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.3" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety" bottom_navy_bar: dependency: "direct main" description: @@ -91,21 +98,21 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.3.10" + version: "1.3.11" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.10.0" + version: "1.10.2" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "6.0.1" built_collection: dependency: transitive description: @@ -119,14 +126,21 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.0.9" + version: "7.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.2" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0-nullsafety" checked_yaml: dependency: transitive description: @@ -154,28 +168,28 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0-nullsafety" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.4.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.15.0-nullsafety.2" color: dependency: transitive description: @@ -190,20 +204,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -224,7 +245,7 @@ packages: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.2.3" + version: "1.2.4" expandable: dependency: "direct main" description: @@ -238,7 +259,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.0-nullsafety" fixnum: dependency: transitive description: @@ -264,7 +285,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "6.0.5" flutter_native_splash: dependency: "direct dev" description: @@ -285,7 +306,7 @@ packages: name: flutter_staggered_grid_view url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.2" flutter_test: dependency: "direct dev" description: flutter @@ -323,7 +344,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.12.2" http_multi_server: dependency: transitive description: @@ -344,7 +365,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.14" intl: dependency: "direct main" description: @@ -365,7 +386,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.3-nullsafety" json_annotation: dependency: transitive description: @@ -386,21 +407,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.10-nullsafety" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0-nullsafety.2" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+3" + version: "0.9.7" + mockito: + dependency: transitive + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.1" nested: dependency: transitive description: @@ -414,14 +442,21 @@ packages: name: node_interop url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.1.1" node_io: dependency: transitive description: name: node_io url: "https://pub.dartlang.org" source: hosted - version: "1.0.1+2" + version: "1.1.1" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.12" package_config: dependency: transitive description: @@ -435,28 +470,28 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.3" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.10.0-nullsafety" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" platform_detect: dependency: transitive description: @@ -477,14 +512,14 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0-nullsafety" provider: dependency: transitive description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.1" + version: "4.3.2+2" pub_semver: dependency: transitive description: @@ -512,7 +547,21 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "0.7.9" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" shelf_web_socket: dependency: transitive description: @@ -531,28 +580,42 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.5" + version: "0.9.6" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.1" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10-nullsafety" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0-nullsafety" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety" stream_transform: dependency: transitive description: @@ -566,21 +629,35 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0-nullsafety" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety" + test: + dependency: transitive + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0-nullsafety.2" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.16" + version: "0.2.19-nullsafety" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.12-nullsafety.2" timing: dependency: transitive description: @@ -601,14 +678,14 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.2" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" + version: "5.5.2" url_launcher_linux: dependency: transitive description: @@ -629,28 +706,35 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "1.0.8" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+6" + version: "0.1.3+1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0-nullsafety.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+14" + version: "0.9.7+15" web_socket_channel: dependency: transitive description: @@ -658,20 +742,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.3" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "4.5.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" sdks: - dart: ">=2.7.0 <3.0.0" - flutter: ">=1.16.0 <2.0.0" + dart: ">=2.10.0-0.0.dev <2.10.0" + flutter: ">=1.17.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4e2d84c..9e9b863 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,12 +34,13 @@ dependencies: auto_size_text: ^2.1.0 flappy_search_bar: ^1.7.2 # Bloc - bloc: ^6.0.1 - flutter_bloc: ^6.0.1 + + bloc: ^6.0.3 + flutter_bloc: ^6.0.5 chopper: ^3.0.3 + bloc_test: ^7.0.3 - -# plugins not needed in app + # plugins not needed in app dev_dependencies: build_runner: ^1.10.0 chopper_generator: ^3.0.5 diff --git a/test/auth_tests/authentication_bloc_test.dart b/test/auth_tests/authentication_bloc_test.dart new file mode 100644 index 0000000..5c58cde --- /dev/null +++ b/test/auth_tests/authentication_bloc_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:mentorship_client/remote/repositories/auth_repository.dart'; +import 'package:mentorship_client/auth/bloc.dart'; + +class MockUserRepository extends Mock implements AuthRepository {} + +void main() { + AuthBloc authenticationBloc; + MockUserRepository userRepository; + + setUp(() { + userRepository = MockUserRepository(); + authenticationBloc = AuthBloc(userRepository); + }); + + tearDown(() { + authenticationBloc?.close(); + }); + + test('initial state is correct', () { + expect(authenticationBloc.state, AuthUninitialized()); + }); + + test('close does not emit new states', () { + expectLater( + authenticationBloc, + emitsInOrder([emitsDone]), + ); + authenticationBloc.close(); + }); + + group('AppStarted', () { + test('emits [unauthenticated] for invalid token', () { + final expectedResponse = [ + AuthUnauthenticated(), + ]; + + when(userRepository.getToken()).thenAnswer((_) => Future.value(null)); + + expectLater( + authenticationBloc, + emitsInOrder(expectedResponse), + ); + + authenticationBloc.add(AppStarted()); + }); + }); + group('LoggedIn', () { + test('emits [loading, authenticated] when token is persisted', () { + final expectedResponse = [ + AuthInProgress(), + AuthAuthenticated(), + ]; + + expectLater( + authenticationBloc, + emitsInOrder(expectedResponse), + ); + + authenticationBloc.add(JustLoggedIn( + 'instance.token', + )); + }); + }); + group('LoggedOut', () { + test('emits [loading, unauthenticated] when token is deleted', () { + final expectedResponse = [ + AuthInProgress(), + AuthUnauthenticated( + justLoggedOut: true, + ), + ]; + + expectLater( + authenticationBloc, + emitsInOrder(expectedResponse), + ); + + authenticationBloc.add(JustLoggedOut()); + }); + }); +} diff --git a/test/auth_tests/authentication_event_test.dart b/test/auth_tests/authentication_event_test.dart new file mode 100644 index 0000000..bc7f01a --- /dev/null +++ b/test/auth_tests/authentication_event_test.dart @@ -0,0 +1,39 @@ +import 'package:mentorship_client/auth/bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppStarted', () { + group('AppStarted', () { + test('props are []', () { + expect(AppStarted().props, []); + }); + + test('toString is "AppStarted"', () { + expect(AppStarted().toString(), 'AppStarted'); + }); + }); + + group('JustLoggedIn', () { + test('props are [token]', () { + expect(JustLoggedIn('token').props, ['token']); + }); + + test('toString is "LoggedIn { token: token }"', () { + expect( + JustLoggedIn('token').toString(), + 'LoggedIn { token: token }', + ); + }); + }); + + group('JustLoggedOut', () { + test('props are []', () { + expect(JustLoggedOut().props, []); + }); + + test('toString is "AuthenticationLoggedOut"', () { + expect(JustLoggedOut().toString(), 'JustLoggedOut'); + }); + }); + }); +} diff --git a/test/login_test/login_bloc_test.dart b/test/login_test/login_bloc_test.dart new file mode 100644 index 0000000..502b5d3 --- /dev/null +++ b/test/login_test/login_bloc_test.dart @@ -0,0 +1,105 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mentorship_client/failure.dart'; +import 'package:mentorship_client/remote/requests/login.dart'; +import 'package:mentorship_client/remote/responses/auth_token.dart'; +import 'package:mentorship_client/screens/login/bloc/bloc.dart'; + +import 'package:mockito/mockito.dart'; +import 'package:mentorship_client/remote/repositories/auth_repository.dart'; +import 'package:mentorship_client/auth/bloc.dart'; + +class MockUserRepository extends Mock implements AuthRepository {} + +class MockAuthenticationBloc extends Mock implements AuthBloc {} + +void main() { + LoginBloc loginBloc; + + MockUserRepository userRepository; + MockAuthenticationBloc authenticationBloc; + Login login; + AuthToken authToken; + setUp(() { + userRepository = MockUserRepository(); + authenticationBloc = MockAuthenticationBloc(); + loginBloc = LoginBloc( + userRepository, + authenticationBloc, + ); + login = Login( + username: 'valid.username', + password: 'valid.password', + ); + + authToken = AuthToken( + 'valid.token', + 7, + ); + }); + + tearDown(() { + loginBloc?.close(); + authenticationBloc?.close(); + }); + + test('initial state is correct', () { + expect(loginBloc.state, LoginInitial()); + }); + + test('close does not emit new states', () { + expectLater( + loginBloc, + emitsInOrder([emitsDone]), + ); + loginBloc.close(); + }); + + group('LoginButtonPressed', () { + blocTest( + 'emits [LoginLoading, LoginSuccess] and token on success', + build: () { + when(userRepository.login( + login, + )).thenAnswer((_) => Future.value( + authToken, + )); + return loginBloc; + }, + act: (bloc) => bloc.add( + LoginButtonPressed( + login, + ), + ), + expect: [ + LoginInProgress(), + LoginSuccess(), + ], + verify: (_) async { + verify(authenticationBloc.add(JustLoggedIn( + authToken.token, + ))).called(1); + }, + ); + + blocTest( + 'emits [LoginLoading, LoginFailure] on failure', + build: () { + when(userRepository.login( + login, + )).thenThrow(Failure('login-error')); + return loginBloc; + }, + act: (bloc) => bloc.add( + LoginButtonPressed(login), + ), + expect: [ + LoginInProgress(), + LoginFailure('login-error'), + ], + verify: (_) async { + verifyNever(authenticationBloc.add(any)); + }, + ); + }); +} diff --git a/test/login_test/login_event_test.dart b/test/login_test/login_event_test.dart new file mode 100644 index 0000000..5352dc3 --- /dev/null +++ b/test/login_test/login_event_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mentorship_client/remote/requests/login.dart'; +import 'package:mentorship_client/screens/login/bloc/bloc.dart'; + + +void main() { + Login login; + setUp(() { + login = Login( + username: 'valid.username', + password: 'valid.password', + ); + }); + group('LoginEvent', () { + group('LoginButtonPressed', () { + test('props are [username, password]', () { + expect( + LoginButtonPressed( + login, + ).props, + [ + login, + ], + ); + }); + }); + }); +} diff --git a/test/login_test/login_state_test.dart b/test/login_test/login_state_test.dart new file mode 100644 index 0000000..0d79cc5 --- /dev/null +++ b/test/login_test/login_state_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mentorship_client/screens/login/bloc/bloc.dart'; + +void main() { + group('LoginState', () { + group('LoginInitial', () { + test('toString is LoginInitial', () { + expect(LoginInitial().toString(), 'LoginInitial'); + }); + }); + + group('LoginInProgress', () { + test('toString is LoginLoading', () { + expect(LoginInProgress().toString(), 'LoginInProgress'); + }); + }); + + group('LoginFailure', () { + test('props are [error]', () { + expect(LoginFailure('message').props, ['message']); + }); + + test('toString is LoginFailure { error: message }', () { + expect( + LoginFailure('message').toString(), + 'LoginFailure { error: message }', + ); + }); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 2a64818..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - - - -void main() {}