Compare commits

...

8 Commits

42 changed files with 2705 additions and 656 deletions

View File

@ -29,43 +29,28 @@ Authentication Bloc for Flutter.
## Features
- UserInterface
* UserFirebase : FirebaseAuth user implementation
- AuthenticationRepositoryInterface
* AuthenticationRepositoryFirebase : FirebaseAuth implementation
- ExceptionsInterface
* ExceptionsFirebase : FirebaseAuth Exception parsing implementation
- AuthenticationBloc
* Tracks every user changes
- Right after the listener has been registered.
- When a user is signed in.
- When the current user is signed out.
- When there is a change in the current user's token.
- On `refresh()`
* Start/Stop listening on demand
- `start()` to listen to user changes
- `stop()` to cancel listener
- SignUpCubit
* Handles email/password validation and password confirmation
* Handles register with email/password
* Handles custom form fields thanks `wyatt_form_bloc`
- Use `entries` to pass a `FormData` object
- You can use several pre configured `FormInput` for validation
- You can use `updateFormData()` to change FormData and validators during runtime (intersection, union, difference or replace)
- SignInCubit
* Handles email/password validation
* Handles login with email/password
- EmailVerificationCubit
* Handles send email verification process
* Handles email verification check
- PasswordResetCubit
* Handles send password reset email process
- Builders
* AuthenticationBuilder to build widgets on user state changes
- Wyatt Architecture
- Entities:
- Account : AccountModel -> Contains account information from provider
- AccountWrapper : AccountWrapperModel -> Contains account and extra data.
- Data Sources:
- Local:
- Cached Authentication Data : AuthenticationCacheDataSourceImpl -> Provides a cache implementation
- Remote:
- Remote Authentication Data : AuthenticationFirebaseDataSourceImpl -> Provides a proxy to FirebaseAuth
- Repositories:
- AuthenticationRepository : AuthenticationRepositoryImpl -> Provides all authentication methods
- Features:
- Authentication:
- AuthenticationBuilder : widget to build reactive view from authentication state
- AuthenticationCubit : tracks every auth changes, have sign out capability.
- SignUp:
- SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign up
- SignIn:
- SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign in
- Consistent
* Every class have same naming convention
- Tested
* Partially tested with *bloc_test*
## Getting started
@ -77,227 +62,4 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
## Usage
Create an authentication repository:
```dart
final AuthenticationRepositoryInterface _authenticationRepository = AuthenticationRepositoryFirebase();
```
Create an authentication cubit:
```dart
final AuthenticationCubit _authenticationCubit = AuthenticationCubit(
authenticationRepository: _authenticationRepository,
);
```
Create a sign up cubit:
```dart
final SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
);
```
You can use `AuthenticationBloc` to route your app.
```dart
return MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthenticationRepositoryInterface>(
create: (context) => _authenticationRepository,
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthenticationCubit>(
create: (context) => _authenticationCubit..init(),
),
BlocProvider<SignUpCubit>(
create: (context) => _signUpCubit,
),
],
child: const AppView(),
),
);
```
> Don't forget to call `init()` on authentication cubit.
And in `AppView` use an `AuthenticationBuilder`:
```dart
AuthenticationBuilder(
unknown: (context) => const LoadingPage(),
unauthenticated: (context) => const LoginPage(),
authenticated: (context, user, userData) => const HomePage(),
)
```
To create a `SignInCubit` you'll need the same `AuthenticationRepository`, you can use the `context`:
```dart
BlocProvider(
create: (_) => SignInCubit(context.read<AuthenticationRepositoryInterface>()),
child: const LoginForm(),
),
```
> In practice it's better to create it in the main `MultiBlocProvider` because the LoginPage can be destroyed, and cubit closed, before login flow ends
## Recipes
### Password confirmation
In this recipe we'll se how to create a custom `FormEntry` to confirm password.
First, create an entry at the SignUpCubit creation:
```dart
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
entries: const FormData([
FormEntry('form_field_confirmPassword', ConfirmedPassword.pure()),
]),
);
```
Then, in the sign up form, create an input for password confirmation:
- `ConfirmedPassword` validator need password value and confirm password value to compare.
```dart
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (confirmPassword) => context
.read<SignUpCubit>()
.dataChanged(
'form_field_confirmPassword',
ConfirmedPassword.dirty(
password: context.read<SignUpCubit>().state.password.value,
value: confirmPassword,
),
),
obscureText: true,
decoration: InputDecoration(
labelText: 'confirm password',
errorText: state.data!.input('form_field_confirmPassword').invalid
? 'passwords do not match'
: null,
),
);
},
);
```
> `form_field_confirmPassword` is the field identifier used in all application to retrieve data. You can use a constant to avoid typos.
You'll need to update password input to update confirm state on password update !
```dart
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<SignUpCubit>().passwordChanged(password);
context.read<SignUpCubit>().dataChanged(
'form_field_confirmPassword',
ConfirmedPassword.dirty(
password: password,
value: context
.read<SignUpCubit>()
.state
.data!
.input('form_field_confirmPassword')
.value,
),
);
},
obscureText: true,
decoration: InputDecoration(
labelText: 'password',
errorText: state.password.invalid ? 'invalid password' : null,
),
);
},
);
```
> Here you call standard `passwordChanged()` AND `dataChanged()`.
And voilà !
### Create Firestore Document on Sign Up
In this recipe we'll se how to create a Firestore Document on sign up success.
First create a callback function:
```dart
Future<void> onSignUpSuccess(SignUpState state, String? uid) async {
if (uid != null) {
final user = {
'uid': uid,
'email': state.email.value,
...state.data.toMap(),
};
await FirebaseFirestore.instance.collection('users').doc(uid).set(user);
}
}
```
Then create SignUpCubit with custom entries and register callback:
```dart
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
entries: const FormData([
FormEntry('form_field_name', Name.pure(), fieldName: 'name'),
FormEntry('form_field_phone', Phone.pure(), fieldName: 'phone'),
FormEntry('form_field_confirmPassword', ConfirmedPassword.pure(), export: false),
]),
onSignUpSuccess: onSignUpSuccess,
);
```
> Use `fieldName` and `export` to control `.toMap()` result on FormData ! Useful to disable exportation of sensible data like passwords.
Create widgets for each inputs:
```dart
class _PhoneInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (phone) => context
.read<SignUpCubit>()
.dataChanged('form_field_phone', Phone.dirty(phone)),
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'phone',
helperText: '',
errorText: state.data!.input('form_field_phone').invalid
? 'invalid phone'
: null,
),
);
},
);
}
}
```
> Create widgets for Name and ConfirmedPassword too.
Then add a sign up button with:
```dart
context.read<SignUpCubit>().signUpFormSubmitted()
```
And voilà, a document with `uid` as id, and fields `email`, `name`, `phone`, `uid` will be create in `users` collection.
// TODO

View File

@ -16,6 +16,7 @@
import 'package:get_it/get_it.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
final getIt = GetIt.I;
@ -23,7 +24,28 @@ abstract class GetItInitializer {
static Future<void> init() async {
getIt
..registerLazySingleton<AuthenticationRemoteDataSource>(
() => AuthenticationFirebaseDataSourceImpl(),
() => AuthenticationMockDataSourceImpl(registeredAccounts: [
Pair(
AccountModel(
uid: '1',
emailVerified: true,
isAnonymous: false,
providerId: 'wyatt',
email: 'toto@test.fr',
),
'toto1234',
),
Pair(
AccountModel(
uid: '2',
emailVerified: false,
isAnonymous: false,
providerId: 'wyatt',
email: 'tata@test.fr',
),
'tata1234',
),
]),
)
..registerLazySingleton<AuthenticationCacheDataSource<int>>(
() => AuthenticationCacheDataSourceImpl<int>(),

View File

@ -3,12 +3,11 @@
// -----
// File: sign_in_form.dart
// Created Date: 19/08/2022 15:24:37
// Last Modified: Thu Nov 10 2022
// Last Modified: Fri Nov 11 2022
// -----
// Copyright (c) 2022
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
@ -73,16 +72,12 @@ class SignInForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<SignInCubit<int>, SignInState>(
listener: (context, state) {
if (state.status.isSubmissionFailure) {
ScaffoldMessenger.of(context)
return SignInListener<int>(
onError: (context, status, errorMessage) => ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Sign In Failure')),
);
}
},
SnackBar(content: Text(errorMessage ?? 'Sign In Failure')),
),
child: SingleChildScrollView(
child: Column(
children: [

View File

@ -3,13 +3,12 @@
// -----
// File: sign_up_form.dart
// Created Date: 19/08/2022 14:41:08
// Last Modified: Thu Nov 10 2022
// Last Modified: Fri Nov 11 2022
// -----
// Copyright (c) 2022
import 'package:example_router/core/constants/form_field.dart';
import 'package:flutter/material.dart' hide FormField;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
@ -110,16 +109,13 @@ class SignUpForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<SignUpCubit<int>, SignUpState>(
listener: (context, state) {
if (state.status.isSubmissionFailure) {
return SignUpListener<int>(
onError: (context, status, errorMessage) =>
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')),
);
}
},
SnackBar(content: Text(errorMessage ?? 'Sign Up Failure')),
),
child: SingleChildScrollView(
child: Column(
children: [
@ -132,7 +128,6 @@ class SignUpForm extends StatelessWidget {
_SignUpButton(),
],
),
),
);
));
}
}

View File

@ -17,4 +17,5 @@
abstract class AuthFormName {
static const String signUpForm = 'wyattSignUpForm';
static const String signInForm = 'wyattSignInForm';
static const String passwordResetForm = 'wyattPasswordResetForm';
}

View File

@ -23,6 +23,9 @@ abstract class AuthenticationFailureInterface extends AppException
String code;
String msg;
@override
String get message => msg;
AuthenticationFailureInterface(this.code, this.msg);
AuthenticationFailureInterface.fromCode(this.code)
: msg = 'An unknown error occurred.';
@ -246,3 +249,10 @@ abstract class SignOutFailureInterface extends AuthenticationFailureInterface {
/// {@macro sign_out_failure}
SignOutFailureInterface.fromCode(super.code) : super.fromCode();
}
abstract class GetIdTokenFailureInterface
extends AuthenticationFailureInterface {
GetIdTokenFailureInterface(super.code, super.msg);
GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode();
}

View File

@ -270,3 +270,10 @@ class SignOutFailureFirebase extends SignOutFailureInterface {
SignOutFailureFirebase.fromCode(super.code) : super.fromCode();
}
class GetIdTokenFailureFirebase extends GetIdTokenFailureInterface {
GetIdTokenFailureFirebase([String? code, String? msg])
: super(code ?? 'unknown', msg ?? 'An unknown error occurred.');
GetIdTokenFailureFirebase.fromCode(super.code) : super.fromCode();
}

View File

@ -16,3 +16,4 @@
export 'local/authentication_cache_data_source_impl.dart';
export 'remote/authentication_firebase_data_source_impl.dart';
export 'remote/authentication_mock_data_source_impl.dart';

View File

@ -15,7 +15,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
@ -28,7 +27,18 @@ class AuthenticationFirebaseDataSourceImpl
Account _mapper(User user) => AccountModel(
uid: user.uid,
emailVerified: user.emailVerified,
isAnonymous: user.isAnonymous,
providerId: user.providerData.first.providerId,
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime,
isNewUser: (user.metadata.creationTime != null &&
user.metadata.lastSignInTime != null)
? user.metadata.lastSignInTime! == user.metadata.creationTime!
: null,
email: user.email,
phoneNumber: user.phoneNumber,
photoURL: user.photoURL,
);
@override
@ -55,6 +65,8 @@ class AuthenticationFirebaseDataSourceImpl
}
@override
/// {@macro signup}
Future<Account> signUp({
required String email,
required String password,
@ -95,9 +107,10 @@ class AuthenticationFirebaseDataSourceImpl
} else {
throw Exception(); // Get caught just after.
}
} on FirebaseAuthException catch (e) {
throw GetIdTokenFailureFirebase.fromCode(e.code);
} catch (_) {
// TODO(hpcl): implement a non ambiguous exception for this case
throw ServerException();
throw GetIdTokenFailureFirebase();
}
}
@ -107,4 +120,83 @@ class AuthenticationFirebaseDataSourceImpl
final Account? account = (user.isNotNull) ? _mapper(user!) : null;
return account;
});
@override
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
}) async {
try {
await _firebaseAuth.confirmPasswordReset(
code: code,
newPassword: newPassword,
);
} on FirebaseAuthException catch (e) {
throw ConfirmPasswordResetFailureFirebase.fromCode(e.code);
} catch (_) {
throw ConfirmPasswordResetFailureFirebase();
}
}
@override
Future<void> sendEmailVerification() async {
try {
await _firebaseAuth.currentUser!.sendEmailVerification();
} on FirebaseAuthException catch (e) {
throw SendEmailVerificationFailureFirebase.fromCode(e.code);
} catch (_) {
throw SendEmailVerificationFailureFirebase();
}
}
@override
Future<void> sendPasswordResetEmail({required String email}) async {
try {
await _firebaseAuth.sendPasswordResetEmail(email: email);
} on FirebaseAuthException catch (e) {
throw SendPasswordResetEmailFailureFirebase.fromCode(e.code);
} catch (_) {
throw SendPasswordResetEmailFailureFirebase();
}
}
@override
Future<Account> signInAnonymously() async {
try {
final userCredential = await _firebaseAuth.signInAnonymously();
final user = userCredential.user;
if (user.isNotNull) {
return _mapper(user!);
} else {
throw Exception(); // Get caught just after.
}
} on FirebaseAuthException catch (e) {
throw SignInAnonymouslyFailureFirebase.fromCode(e.code);
} catch (_) {
throw SignInAnonymouslyFailureFirebase();
}
}
@override
Future<bool> verifyPasswordResetCode({required String code}) async {
try {
final email = await _firebaseAuth.verifyPasswordResetCode(code);
return email.isNotNullOrEmpty;
} on FirebaseAuthException catch (e) {
throw VerifyPasswordResetCodeFailureFirebase.fromCode(e.code);
} catch (_) {
throw VerifyPasswordResetCodeFailureFirebase();
}
}
@override
Future<void> refresh() async {
try {
await _firebaseAuth.currentUser!.reload();
} on FirebaseAuthException catch (e) {
throw RefreshFailureFirebase.fromCode(e.code);
} catch (_) {
throw RefreshFailureFirebase();
}
}
}

View File

@ -0,0 +1,204 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'dart:math';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
Pair<Account, String>? _connectedMock;
Pair<Account, String>? _registeredMock;
final StreamController<Account?> _streamAccount = StreamController();
final List<Pair<Account, String>>? registeredAccounts;
final String idToken;
AuthenticationMockDataSourceImpl({
this.idToken = 'fake-id-token',
this.registeredAccounts,
});
Future<void> _randomDelay() async {
await Future<void>.delayed(
Duration(milliseconds: Random().nextInt(400) + 200),
);
return;
}
@override
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
}) async {
await _randomDelay();
}
@override
Future<String> getIdentityToken() async {
await _randomDelay();
return idToken;
}
@override
Future<void> refresh() async {
await _randomDelay();
if (_connectedMock.isNull) {
throw RefreshFailureFirebase();
}
final refresh = DateTime.now();
final mock = (_connectedMock?.left as AccountModel?)
?.copyWith(lastSignInTime: refresh);
_connectedMock = _connectedMock?.copyWith(left: mock);
_streamAccount.add(mock);
}
@override
Future<void> sendEmailVerification() async {
await _randomDelay();
if (_connectedMock.isNotNull) {
final refresh = DateTime.now();
final mock = (_connectedMock?.left as AccountModel?)?.copyWith(
emailVerified: false,
lastSignInTime: refresh,
);
_streamAccount.add(mock);
_connectedMock = _connectedMock?.copyWith(left: mock);
return;
}
throw SendEmailVerificationFailureFirebase();
}
@override
Future<void> sendPasswordResetEmail({required String email}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
return;
}
}
if (_registeredMock.isNotNull) {
if (_registeredMock?.left?.email != email) {
throw SendPasswordResetEmailFailureFirebase();
}
return;
}
throw SendPasswordResetEmailFailureFirebase();
}
@override
Future<Account> signInAnonymously() async {
await _randomDelay();
final creation = DateTime.now();
final mock = AccountModel(
uid: 'mock-id-anom',
emailVerified: false,
isAnonymous: true,
providerId: 'wyatt',
creationTime: creation,
lastSignInTime: creation,
);
_streamAccount.add(mock);
_connectedMock = _connectedMock?.copyWith(left: mock);
return Future.value(mock);
}
@override
Future<Account> signInWithEmailAndPassword({
required String email,
required String password,
}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
final account = accounts?.first;
if (account?.right != password) {
throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
}
_streamAccount.add(account!.left);
_connectedMock = account.copyWith();
return account.left!;
}
}
if (_registeredMock.isNotNull) {
if (_registeredMock?.left?.email != email) {
throw SignInWithCredentialFailureFirebase.fromCode('user-not-found');
}
if (_registeredMock?.right != password) {
throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
}
_streamAccount.add(_registeredMock!.left);
_connectedMock = _registeredMock!.copyWith();
return _registeredMock!.left!;
}
throw SignInWithCredentialFailureFirebase();
}
@override
Future<void> signOut() async {
_connectedMock = null;
_streamAccount.add(null);
}
@override
Future<Account> signUp({
required String email,
required String password,
}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
'email-already-in-use',
);
}
}
if (_registeredMock?.left?.email == email) {
throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
'email-already-in-use',
);
}
final creation = DateTime.now();
final mock = AccountModel(
uid: 'mock-id-email',
emailVerified: false,
isAnonymous: false,
providerId: 'wyatt',
email: email,
creationTime: creation,
lastSignInTime: creation,
);
_streamAccount.add(mock);
_registeredMock = Pair(mock, password);
return Future.value(mock);
}
@override
Stream<Account?> streamAccount() => _streamAccount.stream.asBroadcastStream();
@override
Future<bool> verifyPasswordResetCode({required String code}) async {
await _randomDelay();
return true;
}
}

View File

@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
@ -16,12 +17,71 @@
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
class AccountModel implements Account {
class AccountModel extends Account {
@override
final String uid;
@override
final String? email;
AccountModel({required this.uid, required this.email});
@override
final DateTime? creationTime;
@override
final bool emailVerified;
@override
final bool isAnonymous;
@override
final bool? isNewUser;
@override
final DateTime? lastSignInTime;
@override
final String? phoneNumber;
@override
final String? photoURL;
@override
final String providerId;
AccountModel({
required this.uid,
required this.emailVerified,
required this.isAnonymous,
required this.providerId,
this.lastSignInTime,
this.creationTime,
this.isNewUser,
this.email,
this.phoneNumber,
this.photoURL,
});
AccountModel copyWith({
String? uid,
String? email,
DateTime? creationTime,
bool? emailVerified,
bool? isAnonymous,
bool? isNewUser,
DateTime? lastSignInTime,
String? phoneNumber,
String? photoURL,
String? providerId,
}) => AccountModel(
uid: uid ?? this.uid,
email: email ?? this.email,
creationTime: creationTime ?? this.creationTime,
emailVerified: emailVerified ?? this.emailVerified,
isAnonymous: isAnonymous ?? this.isAnonymous,
isNewUser: isNewUser ?? this.isNewUser,
lastSignInTime: lastSignInTime ?? this.lastSignInTime,
phoneNumber: phoneNumber ?? this.phoneNumber,
photoURL: photoURL ?? this.photoURL,
providerId: providerId ?? this.providerId,
);
}

View File

@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
@ -24,4 +25,12 @@ class AccountWrapperModel<T> extends AccountWrapper<T> {
final T? data;
AccountWrapperModel(this.account, this.data);
AccountWrapperModel<T> copyWith({
Account? account,
T? data,
}) => AccountWrapperModel<T>(
account ?? this.account,
data ?? this.data,
);
}

View File

@ -132,7 +132,7 @@ class AuthenticationRepositoryImpl<T extends Object>
);
await dataResult.foldAsync(
_authenticationLocalDataSource.storeData,
(error) => throw error,
(error) async => error,
);
}
return account;
@ -208,4 +208,70 @@ class AuthenticationRepositoryImpl<T extends Object>
AccountWrapperModel<T>(account, null),
);
});
@override
FutureResult<void> confirmPasswordReset({
required String code,
required String newPassword,
}) =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.confirmPasswordReset(
code: code,
newPassword: newPassword,
);
},
(error) => error,
);
@override
FutureResult<void> sendEmailVerification() =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.sendEmailVerification();
},
(error) => error,
);
@override
FutureResult<void> sendPasswordResetEmail({required String email}) =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.sendPasswordResetEmail(
email: email,
);
},
(error) => error,
);
@override
FutureResult<Account> signInAnonymously() =>
Result.tryCatchAsync<Account, AppException, AppException>(
() async {
final account =
await _authenticationRemoteDataSource.signInAnonymously();
return account;
},
(error) => error,
);
@override
FutureResult<bool> verifyPasswordResetCode({required String code}) =>
Result.tryCatchAsync<bool, AppException, AppException>(
() async {
final response = await _authenticationRemoteDataSource
.verifyPasswordResetCode(code: code);
return response;
},
(error) => error,
);
@override
FutureResult<void> refresh() =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.refresh();
},
(error) => error,
);
}

View File

@ -30,7 +30,22 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource {
Future<void> signOut();
Future<void> refresh();
Stream<Account?> streamAccount();
Future<String> getIdentityToken();
Future<void> sendEmailVerification();
Future<void> sendPasswordResetEmail({required String email});
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
});
Future<bool> verifyPasswordResetCode({required String code});
Future<Account> signInAnonymously();
}

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:equatable/equatable.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
abstract class Account extends Entity {
abstract class Account extends Equatable implements Entity {
/// The user's unique ID.
String get uid;
@ -24,4 +25,59 @@ abstract class Account extends Entity {
///
/// Will be `null` if signing in anonymously.
String? get email;
/// Returns whether the users email address has been verified.
///
/// To send a verification email, see `SendEmailVerification`.
bool get emailVerified;
/// Returns whether the user is a anonymous.
bool get isAnonymous;
/// Returns the users account creation time.
///
/// When this account was created as dictated by the server clock.
DateTime? get creationTime;
/// When the user last signed in as dictated by the server clock.
DateTime? get lastSignInTime;
/// Returns the users phone number.
///
/// This property will be `null` if the user has not signed in or been has
/// their phone number linked.
String? get phoneNumber;
/// Returns a photo URL for the user.
///
/// This property will be populated if the user has signed in or been linked
/// with a 3rd party OAuth provider (such as Google).
String? get photoURL;
/// The provider ID for the user.
String get providerId;
/// Whether the user account has been recently created.
bool? get isNewUser;
@override
List<Object?> get props => [
uid,
email,
emailVerified,
isAnonymous,
creationTime,
lastSignInTime,
phoneNumber,
photoURL,
providerId,
isNewUser,
];
@override
String toString() => 'AccountModel(uid: $uid, email: $email, '
'creationTime: $creationTime, emailVerified: $emailVerified, '
'isAnonymous: $isAnonymous, isNewUser: $isNewUser, lastSignInTime: '
'$lastSignInTime, phoneNumber: $phoneNumber, photoURL: $photoURL, '
'providerId: $providerId)';
}

View File

@ -14,10 +14,17 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:equatable/equatable.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
abstract class AccountWrapper<T> extends Entity {
abstract class AccountWrapper<T> extends Equatable implements Entity {
Account? get account;
T? get data;
@override
List<Object?> get props => [account, data];
@override
String toString() => 'AccountWrapper($account, data: $data)';
}

View File

@ -22,18 +22,83 @@ import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
abstract class AuthenticationRepository<T> extends BaseRepository {
FormRepository get formRepository;
/// {@template signup}
/// Creates a new user with the provided [email] and [password].
///
/// Returns the newly created user's unique identifier.
///
/// Throws a SignUpWithEmailAndPasswordFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureResult<Account> signUp({
required String email,
required String password,
});
/// {@template send_email_verification}
/// Sends verification email to the account email.
///
/// Throws a SendEmailVerificationFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<void> sendEmailVerification();
/// {@template send_password_reset_email}
/// Sends a password reset email to the provided [email].
///
/// Throws a SendPasswordResetEmailFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<void> sendPasswordResetEmail({required String email});
/// {@template confirm_password_reset}
/// Confirms the password reset with the provided [newPassword] and [code].
///
/// Throws a ConfirmPasswordResetFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<void> confirmPasswordReset({
required String code,
required String newPassword,
});
/// {@template verify_password_reset_code}
/// Verify password reset code.
///
/// Throws a VerifyPasswordResetCodeFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<bool> verifyPasswordResetCode({required String code});
/// {@template signin_anom}
/// Sign in anonymously.
///
/// Throws a SignInAnonymouslyFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<Account> signInAnonymously();
/// {@template signin_pwd}
/// Signs in with the provided [email] and [password].
///
/// Throws a SignInWithEmailAndPasswordFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureResult<Account> signInWithEmailAndPassword({
required String email,
required String password,
});
/// {@template signout}
/// Signs out the current user.
/// It also clears the cache and the associated data.
/// {@endtemplate}
FutureResult<void> signOut();
FutureResult<void> refresh();
/// {@template stream_account}
/// Stream of [AccountWrapper] which will emit the current account when
/// the authentication state changes.
///
/// Emits [AccountWrapper] with null [Account] if the user is not
/// authenticated.
/// {@endtemplate}
Stream<FutureResult<AccountWrapper<T>>> streamAccount();
FutureResult<String> getIdentityToken();

View File

@ -40,16 +40,16 @@ class AuthenticationCubit<Extra> extends Cubit<AuthenticationState<Extra>> {
accountFutureResult.fold(
(value) {
if (value.account.isNotNull) {
emit(AuthenticationState.authenticated(value));
emit(AuthenticationState<Extra>.authenticated(value));
return;
}
_authenticationRepository.destroyCache();
emit(const AuthenticationState.unauthenticated());
emit(AuthenticationState<Extra>.unauthenticated());
return;
},
(error) {
_authenticationRepository.destroyCache();
emit(const AuthenticationState.unauthenticated());
emit(AuthenticationState<Extra>.unauthenticated());
return;
},
);

View File

@ -35,7 +35,7 @@ class AuthenticationState<Extra> extends Equatable {
: this._(status: AuthenticationStatus.unauthenticated);
@override
List<Object?> get props => [status];
List<Object?> get props => [status, accountWrapper];
@override
String toString() =>

View File

@ -0,0 +1,65 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/src/features/email_verification/cubit/email_verification_cubit.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class EmailVerificationBuilder<Extra> extends StatelessWidget {
const EmailVerificationBuilder({
required this.verified,
required this.notVerified,
required this.onError,
this.customBuilder,
super.key,
});
final Widget Function(BuildContext context) verified;
final Widget Function(BuildContext context) notVerified;
final Widget Function(
BuildContext context,
FormStatus status,
String? errorMessage,
) onError;
final Widget Function(BuildContext context, EmailVerificationState)?
customBuilder;
@override
Widget build(BuildContext context) =>
BlocBuilder<EmailVerificationCubit<Extra>, EmailVerificationState>(
builder: (context, state) {
if (customBuilder != null) {
return customBuilder!(context, state);
}
if (state.status.isSubmissionFailure ||
state.status.isSubmissionCanceled) {
return onError(
context,
state.status,
state.errorMessage,
);
}
if (state.isVerified) {
return verified(context);
}
return notVerified(context);
},
);
}

View File

@ -0,0 +1,77 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/src/domain/repositories/repositories.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
part 'email_verification_state.dart';
class EmailVerificationCubit<Extra> extends Cubit<EmailVerificationState> {
final AuthenticationRepository<Extra> _authenticationRepository;
EmailVerificationCubit({
required AuthenticationRepository<Extra> authenticationRepository,
}) : _authenticationRepository = authenticationRepository,
super(const EmailVerificationState());
FutureOr<void> sendEmailVerification() async {
emit(state.copyWith(status: FormStatus.submissionInProgress));
final response = await _authenticationRepository.sendEmailVerification();
emit(
response.fold(
(value) => state.copyWith(status: FormStatus.submissionSuccess),
(error) => state.copyWith(
errorMessage: error.message,
status: FormStatus.submissionFailure,
),
),
);
}
FutureOr<void> checkEmailVerification() async {
emit(state.copyWith(status: FormStatus.submissionInProgress));
final refresh = await _authenticationRepository.refresh();
if (refresh.isErr) {
final refreshError = refresh.err!;
emit(
EmailVerificationState(
errorMessage: refreshError.message,
status: FormStatus.submissionFailure,
),
);
return;
}
final currentAccount = await _authenticationRepository.getAccount();
emit(
currentAccount.fold(
(value) => state.copyWith(
isVerified: value.emailVerified,
status: FormStatus.submissionSuccess,
),
(error) => EmailVerificationState(
errorMessage: error.message,
status: FormStatus.submissionFailure,
),
),
);
}
}

View File

@ -0,0 +1,49 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'email_verification_cubit.dart';
class EmailVerificationState extends Equatable {
final FormStatus status;
final bool isVerified;
final String? errorMessage;
const EmailVerificationState({
this.isVerified = false,
this.status = FormStatus.pure,
this.errorMessage,
});
EmailVerificationState copyWith({
FormStatus? status,
bool? isVerified,
String? errorMessage,
}) =>
EmailVerificationState(
status: status ?? this.status,
isVerified: isVerified ?? this.isVerified,
errorMessage: errorMessage ?? this.errorMessage,
);
@override
List<Object?> get props => [status, isVerified, errorMessage];
@override
String toString() => 'EmailVerificationState(status: ${status.name} '
'${(errorMessage != null) ? " [$errorMessage]" : ""}, '
'isVerified: $isVerified)';
}

View File

@ -0,0 +1,18 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'builder/email_verification_builder.dart';
export 'cubit/email_verification_cubit.dart';

View File

@ -15,5 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication/authentication.dart';
export 'email_verification/email_verification.dart';
export 'password_reset/password_reset.dart';
export 'sign_in/sign_in.dart';
export 'sign_up/sign_up.dart';

View File

@ -0,0 +1,138 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:wyatt_authentication_bloc/src/core/constants/form_field.dart';
import 'package:wyatt_authentication_bloc/src/core/constants/form_name.dart';
import 'package:wyatt_authentication_bloc/src/domain/repositories/authentication_repository.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
part 'password_reset_state.dart';
class PasswordResetCubit<Extra> extends FormDataCubit<PasswordResetState> {
final AuthenticationRepository<Extra> _authenticationRepository;
FormRepository get _formRepository =>
_authenticationRepository.formRepository;
PasswordResetCubit({
required AuthenticationRepository<Extra> authenticationRepository,
}) : _authenticationRepository = authenticationRepository,
super(
PasswordResetState(
form: authenticationRepository.formRepository
.accessForm(AuthFormName.passwordResetForm),
),
);
@override
String get formName => AuthFormName.passwordResetForm;
void emailChanged(String value) {
final Email email = Email.dirty(value);
dataChanged(AuthFormField.email, email);
}
@override
FutureOr<void> dataChanged<Value>(
String key,
FormInputValidator<Value, ValidationError> dirtyValue,
) {
final form = _formRepository.accessForm(formName).clone();
try {
form.updateValidator(key, dirtyValue);
_formRepository.updateForm(form);
} catch (e) {
rethrow;
}
emit(
state.copyWith(form: form, status: form.validate()),
);
}
@override
FutureOr<void> reset() {
final form = state.form.reset();
_formRepository.updateForm(form);
emit(
state.copyWith(form: form, status: form.validate()),
);
}
@override
FutureOr<void> submit() async {
if (!state.status.isValidated) {
return;
}
emit(state.copyWith(status: FormStatus.submissionInProgress));
final form = _formRepository.accessForm(formName);
final email = form.valueOf<String?>(AuthFormField.email);
if (email.isNullOrEmpty) {
emit(
state.copyWith(
errorMessage: 'An error occured while retrieving data from the form.',
status: FormStatus.submissionFailure,
),
);
}
final response = await _authenticationRepository.sendPasswordResetEmail(
email: email!,
);
emit(
response.fold(
(value) => state.copyWith(status: FormStatus.submissionSuccess),
(error) => state.copyWith(
errorMessage: error.message,
status: FormStatus.submissionFailure,
),
),
);
}
@override
FutureOr<void> update(
WyattForm form, {
SetOperation operation = SetOperation.replace,
}) {
final WyattForm current = _formRepository.accessForm(formName).clone();
final WyattForm newForm = operation.operation.call(current, form);
_formRepository.updateForm(newForm);
emit(
state.copyWith(
form: newForm,
status: newForm.validate(),
),
);
}
@override
FutureOr<void> validate() {
emit(
state.copyWith(
status: _formRepository.accessForm(formName).validate(),
),
);
}
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'password_reset_cubit.dart';
class PasswordResetState extends FormDataState {
Email get email => form.validatorOf(AuthFormField.email);
const PasswordResetState({
required super.form,
super.status = FormStatus.pure,
super.errorMessage,
});
PasswordResetState copyWith({
WyattForm? form,
FormStatus? status,
String? errorMessage,
}) =>
PasswordResetState(
form: form ?? this.form,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
@override
List<Object> get props => [email, status];
@override
String toString() => 'PasswordResetState(status: ${status.name} '
'${(errorMessage != null) ? " [$errorMessage]" : ""}, $form)';
}

View File

@ -0,0 +1,17 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'cubit/password_reset_cubit.dart';

View File

@ -0,0 +1,66 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/src/features/sign_in/sign_in.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class SignInListener<Extra> extends StatelessWidget {
const SignInListener({
required this.child,
this.onProgress,
this.onSuccess,
this.onError,
this.customBuilder,
super.key,
});
final void Function(BuildContext context)? onProgress;
final void Function(BuildContext context)? onSuccess;
final void Function(
BuildContext context,
FormStatus status,
String? errorMessage,
)? onError;
final void Function(BuildContext context, SignInState state)? customBuilder;
final Widget child;
@override
Widget build(BuildContext context) =>
BlocListener<SignInCubit<Extra>, SignInState>(
listener: (context, state) {
if (customBuilder != null) {
return customBuilder!(context, state);
}
if (onSuccess != null &&
state.status == FormStatus.submissionSuccess) {
return onSuccess!(context);
}
if (onProgress != null &&
state.status == FormStatus.submissionInProgress) {
return onProgress!(context);
}
if (onError != null &&
(state.status == FormStatus.submissionCanceled ||
state.status == FormStatus.submissionFailure)) {
return onError!(context, state.status, state.errorMessage);
}
},
child: child,
);
}

View File

@ -15,3 +15,4 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'cubit/sign_in_cubit.dart';
export 'listener/sign_in_listener.dart';

View File

@ -0,0 +1,66 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/src/features/sign_up/cubit/sign_up_cubit.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class SignUpListener<Extra> extends StatelessWidget {
const SignUpListener({
required this.child,
this.onProgress,
this.onSuccess,
this.onError,
this.customBuilder,
super.key,
});
final void Function(BuildContext context)? onProgress;
final void Function(BuildContext context)? onSuccess;
final void Function(
BuildContext context,
FormStatus status,
String? errorMessage,
)? onError;
final void Function(BuildContext context, SignUpState state)? customBuilder;
final Widget child;
@override
Widget build(BuildContext context) =>
BlocListener<SignUpCubit<Extra>, SignUpState>(
listener: (context, state) {
if (customBuilder != null) {
return customBuilder!(context, state);
}
if (onSuccess != null &&
state.status == FormStatus.submissionSuccess) {
return onSuccess!(context);
}
if (onProgress != null &&
state.status == FormStatus.submissionInProgress) {
return onProgress!(context);
}
if (onError != null &&
(state.status == FormStatus.submissionCanceled ||
state.status == FormStatus.submissionFailure)) {
return onError!(context, state.status, state.errorMessage);
}
},
child: child,
);
}

View File

@ -15,3 +15,4 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'cubit/sign_up_cubit.dart';
export 'listener/sign_up_listener.dart';

View File

@ -23,22 +23,17 @@ dependencies:
twitter_login: ^4.2.3
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.1.0+1
path: packages/wyatt_form_bloc
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.1.0+1
wyatt_architecture:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_architecture-v0.0.2-dev.0
path: packages/wyatt_architecture
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.0.2
wyatt_type_utils:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.0.3+1
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -17,100 +17,114 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}
implements AuthenticationRepository<int> {}
class MockUser extends Mock implements User {}
class MockAccount extends Mock implements Account {}
void main() {
group('AuthenticationCubit<T>', () {
final MockUser user = MockUser();
late AuthenticationRepository authenticationRepository;
final MockAccount account = MockAccount();
final AccountWrapper<int> wrapper = AccountWrapperModel(account, 10);
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
when(() => authenticationRepository.user).thenAnswer(
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => const Stream.empty(),
);
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.fromIterable([AuthCubitStatus.stoped]),
);
when(
() => authenticationRepository.currentUser,
).thenReturn(user);
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
});
test('initial auth state is `unknown`', () {
expect(
AuthenticationCubit<void>(
AuthenticationCubit<int>(
authenticationRepository: authenticationRepository,
).state,
const AuthenticationState<Never>.unknown(),
);
});
test('initial cubit status is `stoped`', () async {
expect(
await AuthenticationCubit<void>(
authenticationRepository: authenticationRepository,
).status,
AuthCubitStatus.stoped,
);
});
group('UserChanged', () {
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'emits authenticated when user is not empty',
group('ListenForAuthenticationChanges', () {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits authenticated when stream contains account',
setUp: () {
when(() => user.isNotEmpty).thenReturn(true);
when(() => authenticationRepository.user).thenAnswer(
(_) => Stream.value(user),
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Ok(wrapper),
)
]),
);
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.value(AuthCubitStatus.started),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
)..init(),
seed: () => const AuthenticationState.unknown(),
expect: () => [AuthenticationState<void>.authenticated(user, null)],
);
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'emits unauthenticated when user is empty',
setUp: () {
when(() => user.isEmpty).thenReturn(true);
when(() => user.isNotEmpty).thenReturn(false);
when(() => authenticationRepository.user).thenAnswer(
(_) => Stream.value(user),
);
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.value(AuthCubitStatus.started),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
)..init(),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<Never>.unauthenticated()],
);
});
group('LogoutRequested', () {
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'invokes signOut',
setUp: () {
when(
() => authenticationRepository.signOut(),
).thenAnswer((_) async {});
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.logOut(),
seed: () => const AuthenticationState.unknown(),
expect: () => [AuthenticationState<int>.authenticated(wrapper)],
);
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits unauthenticated when account stream is empty',
setUp: () {
when(
() => authenticationRepository.destroyCache(),
).thenAnswer((_) async => const Ok(null));
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Ok(AccountWrapperModel(null, 1)),
)
]),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<int>.unauthenticated()],
);
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits unauthenticated when there is an error in stream',
setUp: () {
when(
() => authenticationRepository.destroyCache(),
).thenAnswer((_) async => const Ok(null));
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Err(ServerException()),
)
]),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<int>.unauthenticated()],
);
});
group('SignOut', () {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'invokes signOut',
setUp: () {
when(
() => authenticationRepository.signOut(),
).thenAnswer((_) async => const Ok(null));
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.signOut(),
verify: (_) {
verify(() => authenticationRepository.signOut()).called(1);
},

View File

@ -18,7 +18,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
class MockUser extends Mock implements User {}
class MockAccount extends Mock implements Account {}
void main() {
group('AuthenticationState', () {
@ -27,29 +27,33 @@ void main() {
const AuthenticationState<void> state =
AuthenticationState.unauthenticated();
expect(state.status, AuthenticationStatus.unauthenticated);
expect(state.user, null);
expect(state.accountWrapper, null);
});
});
group('authenticated', () {
test('has correct status', () {
final MockUser user = MockUser();
final MockAccount account = MockAccount();
final AuthenticationState<void> state =
AuthenticationState.authenticated(user, null);
AuthenticationState.authenticated(
AccountWrapperModel<void>(account, null),
);
expect(state.status, AuthenticationStatus.authenticated);
expect(state.user, user);
expect(state.accountWrapper?.account, account);
});
});
group('authenticated with extra data', () {
test('has correct status', () {
final MockUser user = MockUser();
final MockAccount account = MockAccount();
const String extra = 'AwesomeExtraData';
final AuthenticationState<String> state =
AuthenticationState.authenticated(user, extra);
AuthenticationState.authenticated(
AccountWrapperModel(account, extra),
);
expect(state.status, AuthenticationStatus.authenticated);
expect(state.user, user);
expect(state.extra, extra);
expect(state.accountWrapper?.account, account);
expect(state.accountWrapper?.data, extra);
});
});
});

View File

@ -0,0 +1,249 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
group('EmailVerificationCubit<T>', () {
late MockAccount account;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
when(
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
account = MockAccount();
when(
() => account.emailVerified,
).thenAnswer((_) => true);
});
test('initial state is `false`', () {
expect(
EmailVerificationCubit<int>(
authenticationRepository: authenticationRepository,
).state,
const EmailVerificationState(),
);
});
group('SendVerificationEmail', () {
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes sendEmailVerification,',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.sendEmailVerification(),
verify: (_) {
verify(() => authenticationRepository.sendEmailVerification())
.called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.sendEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.sendEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
});
group('CheckEmailVerification', () {
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes refresh,',
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.checkEmailVerification(),
verify: (_) {
verify(() => authenticationRepository.refresh()).called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes emailVerified,',
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.checkEmailVerification(),
verify: (_) {
verify(() => account.emailVerified).called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success with true if verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
isVerified: true,
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success with false if not verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure on refresh error',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure on get account error',
setUp: () {
when(() => authenticationRepository.getAccount())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
});
});
}

View File

@ -0,0 +1,48 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter_test/flutter_test.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
group('EmailVerificationState', () {
test('supports value comparisons', () {
expect(
const EmailVerificationState(isVerified: true),
const EmailVerificationState(isVerified: true),
);
});
test('returns same object when no properties are passed', () {
expect(
const EmailVerificationState(isVerified: true).copyWith(),
const EmailVerificationState(isVerified: true),
);
});
test('returns object with updated status when status is passed', () {
expect(
const EmailVerificationState(isVerified: true)
.copyWith(status: FormStatus.invalid),
const EmailVerificationState(
isVerified: true,
status: FormStatus.invalid,
),
);
});
});
}

View File

@ -0,0 +1,335 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
const String invalidEmailString = 'invalid';
const String validEmailString = 'test@gmail.com';
group('PasswordResetCubit', () {
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.passwordResetForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
formRepository = MockFormRepository();
when(
() => authenticationRepository.sendPasswordResetEmail(
email: any(named: 'email'),
),
).thenAnswer((_) async => const Ok(null));
when(
() => authenticationRepository.formRepository,
).thenAnswer((_) => formRepository);
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer((_) => form);
});
test('initial state is pure', () {
expect(
PasswordResetCubit(
authenticationRepository: authenticationRepository,
).state,
PasswordResetState(form: form),
);
});
group('emailChanged', () {
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [invalid] when email is invalid',
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.invalid,
),
],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [valid] when email is valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
],
);
});
group('submit', () {
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'does nothing when status is not validated',
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.submit(),
expect: () => const <PasswordResetState>[],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'calls sendPasswordResetEmail with correct email',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
verify: (_) {
verify(
() => authenticationRepository.sendPasswordResetEmail(
email: validEmailString,
),
).called(1);
},
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [submissionInProgress, submissionSuccess] '
'when sendPasswordResetEmail succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionInProgress,
),
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionSuccess,
)
],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [submissionInProgress, submissionFailure] '
'when sendPasswordResetEmail fails',
setUp: () {
when(
() => authenticationRepository.sendPasswordResetEmail(
email: any(named: 'email'),
),
).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionInProgress,
),
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionFailure,
)
],
);
});
});
}

View File

@ -0,0 +1,56 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter_test/flutter_test.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
],
name: AuthFormName.passwordResetForm,
);
group('PasswordResetState', () {
test('supports value comparisons', () {
expect(
PasswordResetState(
form: form,
),
PasswordResetState(form: form),
);
});
test('returns same object when no properties are passed', () {
expect(
PasswordResetState(form: form).copyWith(),
PasswordResetState(form: form),
);
});
test('returns object with updated status when status is passed', () {
expect(
PasswordResetState(form: form).copyWith(status: FormStatus.invalid),
PasswordResetState(
form: form,
status: FormStatus.invalid,
),
);
});
});
}

View File

@ -17,46 +17,61 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock
implements AuthenticationCubit<void> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
const String invalidEmailString = 'invalid';
const Email invalidEmail = Email.dirty(invalidEmailString);
const String validEmailString = 'test@gmail.com';
const Email validEmail = Email.dirty(validEmailString);
const String invalidPasswordString = 'invalid';
const Password invalidPassword = Password.dirty(invalidPasswordString);
const String validPasswordString = 't0pS3cret1234';
const Password validPassword = Password.dirty(validPasswordString);
group('SignInCubit', () {
late AuthenticationRepository authenticationRepository;
late AuthenticationCubit<void> authenticationCubit;
final MockAccount account = MockAccount();
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
authenticationCubit = MockAuthenticationCubit();
formRepository = MockFormRepository();
when(
() => authenticationRepository.signInWithEmailAndPassword(
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenAnswer((_) async {});
).thenAnswer((_) async => Ok(account));
when(
() => authenticationCubit.start(),
).thenReturn(true);
() => authenticationRepository.formRepository,
).thenAnswer((_) => formRepository);
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer((_) => form);
});
test('initial state is SignInState', () {
@ -64,33 +79,90 @@ void main() {
SignInCubit(
authenticationRepository: authenticationRepository,
).state,
const SignInState(),
SignInState(form: form),
);
});
group('emailChanged', () {
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [invalid] when email/password are invalid',
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => const <SignInState>[
SignInState(email: invalidEmail, status: FormStatus.invalid),
expect: () => <SignInState>[
SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
],
);
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const SignInState(password: validPassword),
seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => const <SignInState>[
expect: () => <SignInState>[
SignInState(
email: validEmail,
password: validPassword,
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid,
),
],
@ -98,58 +170,145 @@ void main() {
});
group('passwordChanged', () {
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [invalid] when email/password are invalid',
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.passwordChanged(invalidPasswordString),
expect: () => const <SignInState>[
expect: () => <SignInState>[
SignInState(
password: invalidPassword,
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(invalidPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
],
);
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const SignInState(email: validEmail),
seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.passwordChanged(validPasswordString),
expect: () => const <SignInState>[
expect: () => <SignInState>[
SignInState(
email: validEmail,
password: validPassword,
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid,
),
],
);
});
group('logInWithCredentials', () {
blocTest<SignInCubit, SignInState>(
group('submit', () {
blocTest<SignInCubit<int>, SignInState>(
'does nothing when status is not validated',
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.signInWithEmailAndPassword(),
act: (cubit) => cubit.submit(),
expect: () => const <SignInState>[],
);
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'calls signInWithEmailAndPassword with correct email/password',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const SignInState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
act: (cubit) => cubit.signInWithEmailAndPassword(),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
verify: (_) {
verify(
() => authenticationRepository.signInWithEmailAndPassword(
@ -160,33 +319,85 @@ void main() {
},
);
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [submissionInProgress, submissionSuccess] '
'when signInWithEmailAndPassword succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const SignInState(
seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid,
email: validEmail,
password: validPassword,
),
act: (cubit) => cubit.signInWithEmailAndPassword(),
expect: () => const <SignInState>[
act: (cubit) => cubit.submit(),
expect: () => <SignInState>[
SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
),
SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
)
],
);
blocTest<SignInCubit, SignInState>(
blocTest<SignInCubit<int>, SignInState>(
'emits [submissionInProgress, submissionFailure] '
'when signInWithEmailAndPassword fails',
setUp: () {
@ -195,27 +406,77 @@ void main() {
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenThrow(Exception('oops'));
).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const SignInState(
seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid,
email: validEmail,
password: validPassword,
),
act: (cubit) => cubit.signInWithEmailAndPassword(),
expect: () => const <SignInState>[
act: (cubit) => cubit.submit(),
expect: () => <SignInState>[
SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
),
SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
)
],
);

View File

@ -19,36 +19,38 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
const Email email = Email.dirty('email');
const Password password = Password.dirty('password');
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
group('SignInState', () {
test('supports value comparisons', () {
expect(const SignInState(), const SignInState());
expect(
SignInState(
form: form,
),
SignInState(form: form),
);
});
test('returns same object when no properties are passed', () {
expect(const SignInState().copyWith(), const SignInState());
expect(
SignInState(form: form).copyWith(),
SignInState(form: form),
);
});
test('returns object with updated status when status is passed', () {
expect(
const SignInState().copyWith(status: FormStatus.pure),
const SignInState(),
);
});
test('returns object with updated email when email is passed', () {
expect(
const SignInState().copyWith(email: email),
const SignInState(email: email),
);
});
test('returns object with updated password when password is passed', () {
expect(
const SignInState().copyWith(password: password),
const SignInState(password: password),
SignInState(form: form).copyWith(status: FormStatus.invalid),
SignInState(
form: form,
status: FormStatus.invalid,
),
);
});
});

View File

@ -17,163 +17,298 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock
implements AuthenticationCubit<void> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
const String invalidEmailString = 'invalid';
const Email invalidEmail = Email.dirty(invalidEmailString);
const String validEmailString = 'test@gmail.com';
const Email validEmail = Email.dirty(validEmailString);
const String invalidPasswordString = 'invalid';
const Password invalidPassword = Password.dirty(invalidPasswordString);
const String validPasswordString = 't0pS3cret1234';
const Password validPassword = Password.dirty(validPasswordString);
group('SignUpCubit', () {
late AuthenticationRepository authenticationRepository;
late AuthenticationCubit<void> authenticationCubit;
final MockAccount account = MockAccount();
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signUpForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
authenticationCubit = MockAuthenticationCubit();
formRepository = MockFormRepository();
when(
() => authenticationRepository.signUp(
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenAnswer((_) async => 'uid');
).thenAnswer((_) async => Ok(account));
when(
() => authenticationCubit.start(),
).thenReturn(true);
() => authenticationRepository.formRepository,
).thenAnswer((_) => formRepository);
when(
() => authenticationCubit.stop(),
).thenReturn(true);
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer((_) => form);
});
test('initial state is SignUpState', () {
expect(
SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
).state,
const SignUpState(data: FormData.empty()),
SignUpState(form: form),
);
});
group('emailChanged', () {
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [invalid] when email/password are invalid',
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <SignUpState>[
const SignUpState(
email: invalidEmail,
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
data: FormData.empty(),
),
],
);
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
seed: () => const SignUpState(
password: validPassword,
data: FormData.empty(),
seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => <SignUpState>[
const SignUpState(
email: validEmail,
password: validPassword,
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid,
data: FormData.empty(),
),
],
);
});
group('passwordChanged', () {
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [invalid] when email/password are invalid',
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
act: (cubit) => cubit.passwordChanged(invalidPasswordString),
expect: () => <SignUpState>[
const SignUpState(
password: invalidPassword,
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(invalidPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
data: FormData.empty(),
),
],
);
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
seed: () => const SignUpState(
email: validEmail,
data: FormData.empty(),
seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.passwordChanged(validPasswordString),
expect: () => <SignUpState>[
const SignUpState(
email: validEmail,
password: validPassword,
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid,
data: FormData.empty(),
),
],
);
});
group('signUpFormSubmitted', () {
blocTest<SignUpCubit, SignUpState>(
group('submit', () {
blocTest<SignUpCubit<int>, SignUpState>(
'does nothing when status is not validated',
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
act: (cubit) => cubit.signUpFormSubmitted(),
act: (cubit) => cubit.submit(),
expect: () => const <SignUpState>[],
);
blocTest<SignUpCubit, SignUpState>(
'calls signUp with correct email/password/confirmedPassword',
blocTest<SignUpCubit<int>, SignUpState>(
'calls signUp with correct email/password',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
seed: () => const SignUpState(
seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
act: (cubit) => cubit.signUpFormSubmitted(),
act: (cubit) => cubit.submit(),
verify: (_) {
verify(
() => authenticationRepository.signUp(
@ -184,37 +319,85 @@ void main() {
},
);
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [submissionInProgress, submissionSuccess] '
'when signUp succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
seed: () => const SignUpState(
seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
act: (cubit) => cubit.signUpFormSubmitted(),
act: (cubit) => cubit.submit(),
expect: () => <SignUpState>[
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionInProgress,
),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
const SignUpState(
status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
data: FormData.empty(),
)
],
);
blocTest<SignUpCubit, SignUpState>(
blocTest<SignUpCubit<int>, SignUpState>(
'emits [submissionInProgress, submissionFailure] '
'when signUp fails',
setUp: () {
@ -223,31 +406,77 @@ void main() {
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenThrow(Exception('oops'));
).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
),
seed: () => const SignUpState(
seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
act: (cubit) => cubit.signUpFormSubmitted(),
act: (cubit) => cubit.submit(),
expect: () => <SignUpState>[
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionInProgress,
),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
const SignUpState(
status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
data: FormData.empty(),
)
],
);

View File

@ -19,93 +19,37 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
const Email email = Email.dirty('email');
const String passwordString = 'password';
const Password password = Password.dirty(passwordString);
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
group('SignUpState', () {
test('supports value comparisons', () {
expect(
const SignUpState(
data: FormData.empty(),
),
const SignUpState(
data: FormData.empty(),
SignUpState(
form: form,
),
SignUpState(form: form),
);
});
test('returns same object when no properties are passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(),
const SignUpState(
data: FormData.empty(),
),
SignUpState(form: form).copyWith(),
SignUpState(form: form),
);
});
test('returns object with updated status when status is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(status: FormStatus.pure),
const SignUpState(
data: FormData.empty(),
),
);
});
test('returns object with updated email when email is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(email: email),
const SignUpState(
email: email,
data: FormData.empty(),
),
);
});
test('returns object with updated password when password is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(password: password),
const SignUpState(
password: password,
data: FormData.empty(),
),
);
});
test(
'returns object with updated data'
' when data is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(
data: const FormData(
[
FormInput(
'field',
Name.pure(),
),
],
),
),
const SignUpState(
data: FormData(
[
FormInput(
'field',
Name.pure(),
),
],
),
SignUpState(form: form).copyWith(status: FormStatus.invalid),
SignUpState(
form: form,
status: FormStatus.invalid,
),
);
});