diff --git a/api/lib/middlewares/authentication_validator.dart b/api/lib/middlewares/authentication_validator.dart new file mode 100644 index 0000000..f25c679 --- /dev/null +++ b/api/lib/middlewares/authentication_validator.dart @@ -0,0 +1,25 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:dart_frog/dart_frog.dart'; +import 'package:dart_frog_auth/dart_frog_auth.dart'; +import 'package:hub_domain/hub_domain.dart'; +import 'package:user_repository/user_repository.dart'; +import 'package:very_good_hub_api/models/models.dart'; + +/// Middleware that checks if the request authentication is valid. +/// +/// And sets the session in the request context if so. +Middleware authenticationValidator() => bearerAuthentication( + authenticator: (context, token) async { + final authenticationReposity = context.read(); + final session = Session.fromJson(authenticationReposity.verify(token)); + + final userRepository = context.read(); + final user = await userRepository.findUserById(session.userId); + + if (user != null) { + return ApiSession(user: user, session: session); + } + + return null; + }, + ); diff --git a/api/lib/middlewares/middlewares.dart b/api/lib/middlewares/middlewares.dart index b11b33c..01be7f7 100644 --- a/api/lib/middlewares/middlewares.dart +++ b/api/lib/middlewares/middlewares.dart @@ -1 +1,2 @@ +export 'authentication_validator.dart'; export 'cors_middleware.dart'; diff --git a/api/main.dart b/api/main.dart index 8857503..e855017 100644 --- a/api/main.dart +++ b/api/main.dart @@ -1,18 +1,48 @@ import 'dart:io'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dart_frog/dart_frog.dart'; import 'package:db_client/db_client.dart'; late final DbClient _dbClient; +late final AuthenticationRepository _authenticationRepository; Future init(InternetAddress ip, int port) async { _dbClient = DbClient(); + _authenticationRepository = AuthenticationRepository( + secret: _secret, + issuer: _issuer, + ); } Future run(Handler handler, InternetAddress ip, int port) async { return serve( - handler.use(provider((_) => _dbClient)), + handler + .use( + provider((_) => _dbClient), + ) + .use( + provider((_) => _authenticationRepository), + ), ip, port, ); } + +String get _secret { + final value = Platform.environment['SECRET']; + if (value == null) { + stdout.writeln('No secret provided, running with development settings'); + return 'two is not one, which is not three, but is a number'; + } + return value; +} + +String get _issuer { + final value = Platform.environment['ISSUER']; + if (value == null) { + stdout.writeln('No issuer provided, running with development settings'); + return 'https://localhost:8080'; + } + return value; +} diff --git a/api/packages/authentication_repository/.gitignore b/api/packages/authentication_repository/.gitignore new file mode 100644 index 0000000..526da15 --- /dev/null +++ b/api/packages/authentication_repository/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/api/packages/authentication_repository/README.md b/api/packages/authentication_repository/README.md new file mode 100644 index 0000000..7bb59d2 --- /dev/null +++ b/api/packages/authentication_repository/README.md @@ -0,0 +1,69 @@ +# Authentication Repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![License: MIT][license_badge]][license_link] + +API's authentication repository + +## Installation ๐Ÿ’ป + +**โ— In order to start using Authentication Repository you must have the [Dart SDK][dart_install_link] installed on your machine.** + +Add `authentication_repository` to your `pubspec.yaml`: + +```yaml +dependencies: + authentication_repository: +``` + +Install it: + +```sh +dart pub get +``` + +--- + +## Continuous Integration ๐Ÿค– + +Authentication Repository comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution. + +Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link]. + +--- + +## Running Tests ๐Ÿงช + +To run all unit tests: + +```sh +dart pub global activate coverage 1.2.0 +dart test --coverage=coverage +dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info +``` + +To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). + +```sh +# Generate Coverage Report +genhtml coverage/lcov.info -o coverage/ + +# Open Coverage Report +open coverage/index.html +``` + +[dart_install_link]: https://dart.dev/get-dart +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[mason_link]: https://github.com/felangel/mason +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage +[very_good_ventures_link]: https://verygood.ventures +[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only +[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only +[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/api/packages/authentication_repository/analysis_options.yaml b/api/packages/authentication_repository/analysis_options.yaml new file mode 100644 index 0000000..b388541 --- /dev/null +++ b/api/packages/authentication_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.0.0.yaml diff --git a/api/packages/authentication_repository/coverage_badge.svg b/api/packages/authentication_repository/coverage_badge.svg new file mode 100644 index 0000000..499e98c --- /dev/null +++ b/api/packages/authentication_repository/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/api/packages/authentication_repository/lib/authentication_repository.dart b/api/packages/authentication_repository/lib/authentication_repository.dart new file mode 100644 index 0000000..11bd161 --- /dev/null +++ b/api/packages/authentication_repository/lib/authentication_repository.dart @@ -0,0 +1,4 @@ +/// API's authentication repository +library authentication_repository; + +export 'src/authentication_repository.dart'; diff --git a/api/packages/authentication_repository/lib/src/authentication_repository.dart b/api/packages/authentication_repository/lib/src/authentication_repository.dart new file mode 100644 index 0000000..3f5062f --- /dev/null +++ b/api/packages/authentication_repository/lib/src/authentication_repository.dart @@ -0,0 +1,64 @@ +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; + +/// {@template authentication_failure} +/// Exception throw when verification fails. +/// {@endtemplate} +class AuthenticationFailure implements Exception { + /// {@macro authentication_failure} + const AuthenticationFailure(this.message, this.stackTrace); + + /// The reason verification failed. + final String message; + + /// The stack trace of the exception. + final StackTrace? stackTrace; + + @override + String toString() => 'AuthenticationFailure(message: $message)'; +} + +/// Function signature that builds a JWT. +typedef JWTBuilder = JWT Function(dynamic payload, {String? issuer}); + +/// Function signature that verifies a JWT. +typedef JWTVerifier = JWT Function(String token, JWTKey key); + +/// {@template authentication_repository} +/// API's authentication repository. +/// +/// Responsible for signing and validating user tokens. +/// +/// {@endtemplate} +class AuthenticationRepository { + /// {@macro authentication_repository} + const AuthenticationRepository({ + required String secret, + required String issuer, + JWTBuilder jwtBuilder = JWT.new, + JWTVerifier jwtVerifier = JWT.verify, + }) : _secret = secret, + _issuer = issuer, + _jwtBuilder = jwtBuilder, + _jwtVerifier = jwtVerifier; + + final String _secret; + final String _issuer; + final JWTBuilder _jwtBuilder; + final JWTVerifier _jwtVerifier; + + /// Signs a payload into a token. + String sign(Map payload) { + final jwt = _jwtBuilder(payload, issuer: _issuer); + return jwt.sign(SecretKey(_secret)); + } + + /// Verifies a token and returns the payload. + Map verify(String token) { + try { + final jwt = _jwtVerifier(token, SecretKey(_secret)); + return jwt.payload as Map; + } catch (e, s) { + throw AuthenticationFailure(e.toString(), s); + } + } +} diff --git a/api/packages/authentication_repository/pubspec.yaml b/api/packages/authentication_repository/pubspec.yaml new file mode 100644 index 0000000..18950a9 --- /dev/null +++ b/api/packages/authentication_repository/pubspec.yaml @@ -0,0 +1,14 @@ +name: authentication_repository +description: API's authentication repository +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dev_dependencies: + mocktail: ^0.3.0 + test: ^1.19.2 + very_good_analysis: ^5.0.0 +dependencies: + dart_jsonwebtoken: ^2.9.1 diff --git a/api/packages/authentication_repository/test/src/authentication_repository_test.dart b/api/packages/authentication_repository/test/src/authentication_repository_test.dart new file mode 100644 index 0000000..459f0a0 --- /dev/null +++ b/api/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -0,0 +1,17 @@ +// ignore_for_file: prefer_const_constructors +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('AuthenticationRepository', () { + test('can be instantiated', () { + expect( + AuthenticationRepository( + secret: 'two is not equals to four, neither one', + issuer: 'https://the-issuer.com', + ), + isNotNull, + ); + }); + }); +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 63dfec9..a9b8848 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -7,6 +7,8 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: + authentication_repository: + path: packages/authentication_repository dart_frog: ^1.0.0 dart_frog_auth: ^1.1.0 db_client: diff --git a/api/routes/auth/sign_in.dart b/api/routes/auth/sign_in.dart index 83461cb..a62b668 100644 --- a/api/routes/auth/sign_in.dart +++ b/api/routes/auth/sign_in.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dart_frog/dart_frog.dart'; import 'package:session_repository/session_repository.dart'; import 'package:user_repository/user_repository.dart'; @@ -31,7 +32,14 @@ Future _onPost(RequestContext context) async { if (user != null) { final session = await sessionRepository.createSession(user.id); - return Response.json(body: session.toJson()); + + final authenticationRepository = context.read(); + final signedSession = authenticationRepository.sign(session.toJson()); + return Response.json( + body: { + 'token': signedSession, + }, + ); } else { return Response(statusCode: HttpStatus.unauthorized); } diff --git a/api/routes/hub/_middleware.dart b/api/routes/hub/_middleware.dart index 592907b..3339db0 100644 --- a/api/routes/hub/_middleware.dart +++ b/api/routes/hub/_middleware.dart @@ -1,32 +1,12 @@ import 'package:dart_frog/dart_frog.dart'; -import 'package:dart_frog_auth/dart_frog_auth.dart'; import 'package:db_client/db_client.dart'; import 'package:session_repository/session_repository.dart'; import 'package:user_repository/user_repository.dart'; import 'package:very_good_hub_api/middlewares/middlewares.dart'; -import 'package:very_good_hub_api/models/models.dart'; Handler middleware(Handler handler) { return handler - .use( - bearerAuthentication( - authenticator: (context, token) async { - final sessionRepository = context.read(); - final session = await sessionRepository.sessionFromToken(token); - - if (session != null) { - final userRepository = context.read(); - final user = await userRepository.findUserById(session.userId); - - if (user != null) { - return ApiSession(user: user, session: session); - } - } - - return null; - }, - ), - ) + .use(authenticationValidator()) .use(corsHeaders()) .use(requestLogger()) .use( diff --git a/api/routes/hub/session.dart b/api/routes/hub/session.dart index 6bde0f8..6a48ee0 100644 --- a/api/routes/hub/session.dart +++ b/api/routes/hub/session.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dart_frog/dart_frog.dart'; import 'package:very_good_hub_api/models/models.dart'; @@ -12,5 +13,8 @@ Future onRequest(RequestContext context) async { Future _onGet(RequestContext context) async { final apiSession = context.read(); - return Response.json(body: apiSession.session.toJson()); + final authenticationRepository = context.read(); + + final token = authenticationRepository.sign(apiSession.session.toJson()); + return Response.json(body: {'token': token}); } diff --git a/api/test/lib/middlewares/authentication_validator_test.dart b/api/test/lib/middlewares/authentication_validator_test.dart new file mode 100644 index 0000000..5d98f88 --- /dev/null +++ b/api/test/lib/middlewares/authentication_validator_test.dart @@ -0,0 +1,156 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:dart_frog/dart_frog.dart'; +import 'package:hub_domain/hub_domain.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:user_repository/user_repository.dart'; +import 'package:very_good_hub_api/middlewares/middlewares.dart'; + +class _MockContext extends Mock implements RequestContext { + @override + RequestContext provide(T Function() create) { + return this; + } +} + +class _MockRequest extends Mock implements Request {} + +class _MockAuthenticationRepository extends Mock + implements AuthenticationRepository {} + +class _MockUserRepository extends Mock implements UserRepository {} + +void main() { + group('authenticationValidator', () { + late _MockContext context; + late _MockRequest request; + late _MockAuthenticationRepository authenticationRepository; + late _MockUserRepository userRepository; + + setUp(() { + context = _MockContext(); + request = _MockRequest(); + authenticationRepository = _MockAuthenticationRepository(); + userRepository = _MockUserRepository(); + + when(() => context.read()) + .thenReturn(authenticationRepository); + when(() => context.read()).thenReturn(userRepository); + + when(() => context.request).thenReturn(request); + }); + + test('returns the user when the session is valid', () async { + final session = Session( + id: 'id', + userId: 'userId', + token: '', + createdAt: DateTime.now(), + expiryDate: DateTime.now(), + ); + + const user = User( + id: 'userId', + username: 'username', + name: 'name', + ); + + when(() => authenticationRepository.verify('TOKEN')) + .thenReturn(session.toJson()); + + when(() => userRepository.findUserById('userId')).thenAnswer( + (_) async => user, + ); + + when(() => request.headers).thenReturn( + { + 'Authorization': 'Bearer TOKEN', + }, + ); + + final middleware = authenticationValidator(); + + var called = false; + await middleware((_) { + called = true; + return Response(); + })(context); + + expect(called, isTrue); + }); + + test("don't call the handler when there is no authorization token", () async { + when(() => request.headers).thenReturn( + {}, + ); + + final middleware = authenticationValidator(); + + var called = false; + await middleware((_) { + called = true; + return Response(); + })(context); + + expect(called, isFalse); + }); + + test("don't call the handler when the token is not valid", () async { + when(() => authenticationRepository.verify('TOKEN')) + .thenThrow(Exception()); + + when(() => request.headers).thenReturn( + { + 'Authorization': 'Bearer TOKEN', + }, + ); + + final middleware = authenticationValidator(); + + var called = false; + try { + await middleware((_) { + called = true; + return Response(); + })(context); + } catch (_) { + // Expected + } + + expect(called, isFalse); + }); + + test("returns null when the user don't exists", () async { + final session = Session( + id: 'id', + userId: 'userId', + token: '', + createdAt: DateTime.now(), + expiryDate: DateTime.now(), + ); + + when(() => authenticationRepository.verify('TOKEN')) + .thenReturn(session.toJson()); + + when(() => userRepository.findUserById('userId')).thenAnswer( + (_) async => null, + ); + + when(() => request.headers).thenReturn( + { + 'Authorization': 'Bearer TOKEN', + }, + ); + + final middleware = authenticationValidator(); + + var called = false; + await middleware((_) { + called = true; + return Response(); + })(context); + + expect(called, isFalse); + }); + }); +} diff --git a/api/test/routes/auth/sign_in_test.dart b/api/test/routes/auth/sign_in_test.dart index 61687a4..e970b77 100644 --- a/api/test/routes/auth/sign_in_test.dart +++ b/api/test/routes/auth/sign_in_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dart_frog/dart_frog.dart'; import 'package:hub_domain/hub_domain.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,12 +18,16 @@ class _MockUserRepository extends Mock implements UserRepository {} class _MockSessionRepository extends Mock implements SessionRepository {} +class _MockAuthenticationRepository extends Mock + implements AuthenticationRepository {} + void main() { group('POST /auth/sign_in', () { late RequestContext context; late Request request; late UserRepository userRepository; late SessionRepository sessionRepository; + late AuthenticationRepository authenticationRepository; setUp(() { context = _MockRequestContext(); @@ -34,9 +39,12 @@ void main() { sessionRepository = _MockSessionRepository(); when(() => context.read()) .thenReturn(sessionRepository); + authenticationRepository = _MockAuthenticationRepository(); + when(() => context.read()) + .thenReturn(authenticationRepository); }); - test('returns the session when everything passes', () async { + test('returns the signed session when everything passes', () async { when(() => request.json()).thenAnswer( (_) async => { 'username': 'john.doe', @@ -66,12 +74,15 @@ void main() { createdAt: now, ); + when(() => authenticationRepository.sign(session.toJson())) + .thenReturn('super secured token'); + when(() => sessionRepository.createSession('1')) .thenAnswer((_) async => session); final response = await route.onRequest(context); final json = await response.json(); - expect(json, equals(session.toJson())); + expect(json, equals({'token': 'super secured token'})); }); test('returns 400 when username is missing', () async { diff --git a/api/test/routes/hub/session_test.dart b/api/test/routes/hub/session_test.dart index 877e45a..acda4ab 100644 --- a/api/test/routes/hub/session_test.dart +++ b/api/test/routes/hub/session_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dart_frog/dart_frog.dart'; import 'package:hub_domain/hub_domain.dart'; import 'package:mocktail/mocktail.dart'; @@ -8,6 +9,9 @@ import 'package:very_good_hub_api/models/models.dart'; import '../../../routes/hub/session.dart' as route; +class _MockAuthenticationRepository extends Mock + implements AuthenticationRepository {} + class _MockRequestContext extends Mock implements RequestContext {} class _MockRequest extends Mock implements Request {} @@ -33,18 +37,32 @@ void main() { final apiSession = ApiSession(user: user, session: session); group('GET /session', () { - test('returns 200 with the session', () async { + test('returns 200 with the signed session', () async { final context = _MockRequestContext(); final request = _MockRequest(); + final authenticationRepository = _MockAuthenticationRepository(); when(() => context.request).thenReturn(request); when(() => request.method).thenReturn(HttpMethod.get); when(() => context.read()).thenReturn(apiSession); + when(() => context.read()) + .thenReturn(authenticationRepository); + + when( + () => authenticationRepository.sign( + session.toJson(), + ), + ).thenReturn('super secured session token'); final response = await route.onRequest(context); expect(response.statusCode, equals(200)); - expect(await response.json(), equals(session.toJson())); + expect( + await response.json(), + equals( + {'token': 'super secured session token'}, + ), + ); }); test('returns method not allowed when not GET', () async { diff --git a/hub_app/lib/app/bloc/app_bloc.dart b/hub_app/lib/app/bloc/app_bloc.dart index 1df11ce..c079421 100644 --- a/hub_app/lib/app/bloc/app_bloc.dart +++ b/hub_app/lib/app/bloc/app_bloc.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:token_provider/token_provider.dart'; import 'package:user_repository/user_repository.dart'; @@ -24,8 +23,8 @@ class AppBloc extends Bloc { _sessionSubscription = _authenticationRepository.session.listen((event) { if (event != null) { - _tokenProvider.applyToken(event.token); - add(SessionLoaded(session: event)); + _tokenProvider.applyToken(event); + add(SessionLoaded(sessionToken: event)); } else { add(const SessionLoggedOff()); _tokenProvider.clear(); @@ -35,7 +34,7 @@ class AppBloc extends Bloc { tokenProvider.current.then((value) async { if (value != null) { final session = await _userRepository.getUserSession(); - add(SessionLoaded(session: session)); + add(SessionLoaded(sessionToken: session)); } }); } @@ -43,13 +42,13 @@ class AppBloc extends Bloc { final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository; final TokenProvider _tokenProvider; - late StreamSubscription _sessionSubscription; + late StreamSubscription _sessionSubscription; void _onSessionLoaded( SessionLoaded event, Emitter emit, ) { - emit(AppAuthenticated(session: event.session)); + emit(AppAuthenticated(sessionToken: event.sessionToken)); } void _onSessionLoggedOff( diff --git a/hub_app/lib/app/bloc/app_event.dart b/hub_app/lib/app/bloc/app_event.dart index 1d242c6..a3c61bd 100644 --- a/hub_app/lib/app/bloc/app_event.dart +++ b/hub_app/lib/app/bloc/app_event.dart @@ -5,12 +5,12 @@ abstract class AppEvent extends Equatable { } class SessionLoaded extends AppEvent { - const SessionLoaded({required this.session}); + const SessionLoaded({required this.sessionToken}); - final Session session; + final String sessionToken; @override - List get props => [session]; + List get props => [sessionToken]; } class SessionLoggedOff extends AppEvent { diff --git a/hub_app/lib/app/bloc/app_state.dart b/hub_app/lib/app/bloc/app_state.dart index bce26db..a60f821 100644 --- a/hub_app/lib/app/bloc/app_state.dart +++ b/hub_app/lib/app/bloc/app_state.dart @@ -12,10 +12,10 @@ class AppInitial extends AppState { } class AppAuthenticated extends AppState { - const AppAuthenticated({required this.session}); + const AppAuthenticated({required this.sessionToken}); - final Session session; + final String sessionToken; @override - List get props => [session]; + List get props => [sessionToken]; } diff --git a/hub_app/packages/authentication_repository/lib/src/authentication_repository.dart b/hub_app/packages/authentication_repository/lib/src/authentication_repository.dart index b8757d1..e13640e 100644 --- a/hub_app/packages/authentication_repository/lib/src/authentication_repository.dart +++ b/hub_app/packages/authentication_repository/lib/src/authentication_repository.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:api_client/api_client.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:rxdart/rxdart.dart'; /// {@template authentication_failure} @@ -50,11 +49,11 @@ class AuthenticationRepository { final ApiClient _apiClient; - final _sessionsSubject = BehaviorSubject(); + final _sessionsSubject = BehaviorSubject(); - /// Stream of [Session]s which will emit the current session + /// Stream of session tokens which will emit the current session /// when authentication changes. - Stream get session => _sessionsSubject.stream; + Stream get session => _sessionsSubject.stream; /// Login with [username] and [password] Future login({ @@ -75,9 +74,9 @@ class AuthenticationRepository { if (response.statusCode == HttpStatus.ok) { final json = jsonDecode(response.body) as Map; - final session = Session.fromJson(json); + final token = json['token'] as String?; - _sessionsSubject.add(session); + _sessionsSubject.add(token); } else { throw AuthenticationFailure( cause: 'Authentication failed', diff --git a/hub_app/packages/authentication_repository/test/src/authentication_repository_test.dart b/hub_app/packages/authentication_repository/test/src/authentication_repository_test.dart index f74abc2..0fd4368 100644 --- a/hub_app/packages/authentication_repository/test/src/authentication_repository_test.dart +++ b/hub_app/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -64,7 +64,7 @@ void main() { expect( await authenticationRepository.session.first, - session, + equals(session.token), ); }); diff --git a/hub_app/packages/user_repository/lib/src/user_repository.dart b/hub_app/packages/user_repository/lib/src/user_repository.dart index 33fcda5..9e16dd2 100644 --- a/hub_app/packages/user_repository/lib/src/user_repository.dart +++ b/hub_app/packages/user_repository/lib/src/user_repository.dart @@ -56,7 +56,7 @@ class UserRepository { } /// Gets the session. - Future getUserSession() async { + Future getUserSession() async { final response = await _apiClient.authenticatedGet( 'hub/session', ); @@ -71,7 +71,7 @@ class UserRepository { ); } else { final json = jsonDecode(response.body) as Map; - return Session.fromJson(json); + return json['token'] as String; } } } diff --git a/hub_app/packages/user_repository/test/src/user_repository_test.dart b/hub_app/packages/user_repository/test/src/user_repository_test.dart index df18f74..90edeed 100644 --- a/hub_app/packages/user_repository/test/src/user_repository_test.dart +++ b/hub_app/packages/user_repository/test/src/user_repository_test.dart @@ -91,24 +91,16 @@ void main() { }); group('getUserSession', () { - final session = Session( - id: 'id', - token: 'token', - userId: 'userId', - createdAt: DateTime(2021), - expiryDate: DateTime(2021).subtract(const Duration(days: 1)), - ); - test('returns the user', () async { final response = _MockResponse(); when(() => response.statusCode).thenReturn(HttpStatus.ok); - when(() => response.body).thenReturn(jsonEncode(session.toJson())); + when(() => response.body).thenReturn(jsonEncode({'token': 'TOKEN'})); when(() => apiClient.authenticatedGet('hub/session')) .thenAnswer((_) async => response); final result = await userRepository.getUserSession(); - expect(result, equals(session)); + expect(result, equals('TOKEN')); }); test( diff --git a/hub_app/test/app/bloc/app_bloc_test.dart b/hub_app/test/app/bloc/app_bloc_test.dart index 658d713..a652c2c 100644 --- a/hub_app/test/app/bloc/app_bloc_test.dart +++ b/hub_app/test/app/bloc/app_bloc_test.dart @@ -3,7 +3,6 @@ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:mocktail/mocktail.dart'; import 'package:token_provider/token_provider.dart'; import 'package:user_repository/user_repository.dart'; @@ -21,7 +20,6 @@ void main() { late AuthenticationRepository authenticationRepository; late UserRepository userRepository; late TokenProvider tokenProvider; - final now = DateTime.now(); setUp(() { authenticationRepository = _MockAuthenticationRepository(); @@ -64,32 +62,16 @@ void main() { ), setUp: () { when(() => authenticationRepository.session).thenAnswer( - (_) => Stream.fromIterable( - [ - Session( - id: 'mock-user-id', - userId: 'userId', - token: 'token', - createdAt: now, - expiryDate: now.add(const Duration(days: 1)), - ), - ], - ), + (_) => Stream.fromIterable(['TOKEN']), ); }, expect: () => [ AppAuthenticated( - session: Session( - id: 'mock-user-id', - userId: 'userId', - token: 'token', - createdAt: now, - expiryDate: now.add(const Duration(days: 1)), - ), + sessionToken: 'TOKEN', ), ], verify: (_) { - verify(() => tokenProvider.applyToken('token')).called(1); + verify(() => tokenProvider.applyToken('TOKEN')).called(1); }, ); @@ -101,13 +83,7 @@ void main() { tokenProvider: tokenProvider, ), seed: () => AppAuthenticated( - session: Session( - id: 'mock-user-id', - userId: 'userId', - token: 'token', - createdAt: now, - expiryDate: now.add(const Duration(days: 1)), - ), + sessionToken: 'TOKEN', ), setUp: () { when(() => authenticationRepository.session).thenAnswer( @@ -134,24 +110,12 @@ void main() { setUp: () { when(() => tokenProvider.current).thenAnswer((_) async => 'token'); when(userRepository.getUserSession).thenAnswer( - (_) async => Session( - id: 'mock-user-id', - userId: 'userId', - token: 'token', - createdAt: now, - expiryDate: now.add(const Duration(days: 1)), - ), + (_) async => 'TOKEN', ); }, expect: () => [ AppAuthenticated( - session: Session( - id: 'mock-user-id', - userId: 'userId', - token: 'token', - createdAt: now, - expiryDate: now.add(const Duration(days: 1)), - ), + sessionToken: 'TOKEN', ), ], ); diff --git a/hub_app/test/app/bloc/app_event_test.dart b/hub_app/test/app/bloc/app_event_test.dart index 33e7691..122e4b5 100644 --- a/hub_app/test/app/bloc/app_event_test.dart +++ b/hub_app/test/app/bloc/app_event_test.dart @@ -1,45 +1,32 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:very_good_hub/app/app.dart'; void main() { group('AppEvent', () { group('SessionLoaded', () { - final session1 = Session( - id: '1', - userId: '', - token: '', - createdAt: DateTime.now(), - expiryDate: DateTime.now(), - ); - final session2 = Session( - id: '2', - userId: '', - token: '', - createdAt: DateTime.now(), - expiryDate: DateTime.now(), - ); + const session1 = 'TOKEN_1'; + const session2 = 'TOKEN_2'; test('can be instantiated', () { expect( - SessionLoaded(session: session1), + SessionLoaded(sessionToken: session1), isNotNull, ); }); test('supports equality', () { expect( - SessionLoaded(session: session1), + SessionLoaded(sessionToken: session1), equals( - SessionLoaded(session: session1), + SessionLoaded(sessionToken: session1), ), ); expect( - SessionLoaded(session: session1), + SessionLoaded(sessionToken: session1), isNot( equals( - SessionLoaded(session: session2), + SessionLoaded(sessionToken: session2), ), ), ); diff --git a/hub_app/test/app/bloc/app_state_test.dart b/hub_app/test/app/bloc/app_state_test.dart index c42d7bb..6b6c2db 100644 --- a/hub_app/test/app/bloc/app_state_test.dart +++ b/hub_app/test/app/bloc/app_state_test.dart @@ -1,7 +1,6 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:very_good_hub/app/app.dart'; void main() { @@ -17,33 +16,21 @@ void main() { }); group('AppAuthenticated', () { - final session1 = Session( - id: '1', - userId: '', - token: '', - createdAt: DateTime.now(), - expiryDate: DateTime.now(), - ); - final session2 = Session( - id: '2', - userId: '', - token: '', - createdAt: DateTime.now(), - expiryDate: DateTime.now(), - ); + const session1 = 'TOKEN_1'; + const session2 = 'TOKEN_2'; test('can be instantiated', () { - expect(AppAuthenticated(session: session1), isNotNull); + expect(AppAuthenticated(sessionToken: session1), isNotNull); }); test('supports equality', () { expect( - AppAuthenticated(session: session1), - equals(AppAuthenticated(session: session1)), + AppAuthenticated(sessionToken: session1), + equals(AppAuthenticated(sessionToken: session1)), ); expect( - AppAuthenticated(session: session1), - isNot(equals(AppAuthenticated(session: session2))), + AppAuthenticated(sessionToken: session1), + isNot(equals(AppAuthenticated(sessionToken: session2))), ); }); }); diff --git a/hub_app/test/app/view/home_view_test.dart b/hub_app/test/app/view/home_view_test.dart index 27e7324..d6e7e70 100644 --- a/hub_app/test/app/view/home_view_test.dart +++ b/hub_app/test/app/view/home_view_test.dart @@ -3,7 +3,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:very_good_hub/app/app.dart'; @@ -37,15 +36,7 @@ void main() { 'renders the profile button when there is a session', (tester) async { mockState( - AppAuthenticated( - session: Session( - id: '1', - userId: '1', - token: 'token', - expiryDate: DateTime.now().add(const Duration(days: 1)), - createdAt: DateTime.now(), - ), - ), + AppAuthenticated(sessionToken: 'TOKEN_1'), ); await tester.pumpSuject(appBloc: appBloc); expect(find.text('Profile.'), findsOneWidget); @@ -56,15 +47,7 @@ void main() { 'navigates to the profile page when the profile button is tapped', (tester) async { mockState( - AppAuthenticated( - session: Session( - id: '1', - userId: '1', - token: 'token', - expiryDate: DateTime.now().add(const Duration(days: 1)), - createdAt: DateTime.now(), - ), - ), + AppAuthenticated(sessionToken: 'TOKEN_1'), ); final mockNavigator = MockNavigator(); diff --git a/hub_app/test/auth/view/auth_view_test.dart b/hub_app/test/auth/view/auth_view_test.dart index 8e01999..33b69c6 100644 --- a/hub_app/test/auth/view/auth_view_test.dart +++ b/hub_app/test/auth/view/auth_view_test.dart @@ -4,7 +4,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hub_domain/hub_domain.dart'; import 'package:mocktail/mocktail.dart'; import 'package:very_good_hub/app/app.dart'; import 'package:very_good_hub/auth/auth.dart'; @@ -43,13 +42,7 @@ void main() { setUpAll(() { registerFallbackValue( SessionLoaded( - session: Session( - id: '', - userId: '', - token: '', - expiryDate: DateTime.now(), - createdAt: DateTime.now(), - ), + sessionToken: '', ), ); registerFallbackValue(