I try to write an application that insert record into mongodb.
First look at my test:
test('Password test failed, not strong enough.', () {
Account.create({'name': 'eric', 'email': 'koston#mail.com', 'password': 'Test'})
.catchError((err) {
expect(err, throwsA(new isInstanceOf<DuplicateError>()));
});
});
This test should be failed, because the password is not strong enough. And the code, that try to insert record.
static Future<String> create(Map account) {
var completer = new Completer();
String hashed_password;
var self = new Account();
if(self._signUpKeys.length != account.length) {
return completer.completeError(new LengthError(I18n.instance.getTextByMap('TEXT1')));
}
for(var singUpKey in self._signUpKeys) {
if (!account.containsKey(singUpKey)) {
return completer.completeError(new ArgumentError(I18n.instance.getTextByMap('TEXT1')));
}
}
// Try to find duplication
Future.wait([findAccountByField('name', account['name']),
findAccountByField('email', account['email'])])
.then((Iterable<Map<String, String>> lists) {
// Check of any duplications
lists.forEach((value){
value.forEach((String key, String value) {
switch(key) {
case('name'):
return completer.completeError(new DuplicateError(
I18n.instance.getTextWithMarker('TEXT2', {'&1': value})));
case('email'):
return completer.completeError(new DuplicateError(
I18n.instance.getTextWithMarker('TEXT3', {'&1': value})));
}
});
hashed_password = Account.generateHashPassword(account['password']);
self._insert(self._fillDbFields(name: account['name'], email: account['email'], hashed_password: hashed_password,
created_at: new DateTime.now(), activated: false))
.then((result) => completer.complete(result));
});
})
.catchError((err) {
completer.completeError(err);
});
return completer.future;
}
this allocation will thrown an error, because the password is not according to security requirement.
hashed_password = Account.generateHashPassword(account['password']);
and this part should catch error and complete it.
.catchError((err) {
completer.completeError(err);
});
but in the test, I've got NoSuchMethodError. Why here, the error object is not pass over to test? What i do here wrong?
I think you should check what value lists has here
Future.wait([findAccountByField('name', account['name']),
findAccountByField('email', account['email'])])
.then((Iterable<Map<String, String>> lists) {
if it is null you can't call forEach on it
Related
I'm working on a test CRUD application using Bloc for state management and sqflite database. I was asked to implement BDD testing for the app but I have no idea about BDD testing. From what I've learned so far I tryed to implement a simple scenario for the start which is landing on the home screen, but I'm getting this error when I run the test.
Also I don't know exactly how to mock my database to test the four main Create, Read, Update, and Delete functionalities.
I'm using getIt for dependency injection, Mocktail, and bdd_widget_test.
It is the scenario that I wrote in the .feature file:
Feature: Add Feature
Scenario: Landing on the home screen
Given the app is running
Then I see enabled elevated button
And it's the logic for the test.dart file where I get the mentioned exception:
class MockGetAllCustomers extends Mock implements GetAllCustomersUsecase {}
class MockAddCustomer extends Mock implements AddCustomerUsecase {}
class MockUpdateCustomer extends Mock implements UpdateCustomerUsecase {}
class MockDeleteCustomer extends Mock implements DeleteCustomerUsecase {}
class MockDbHelper extends Mock implements DBHelper {}
void main() {
final GetIt getIt = GetIt.instance;
late CustomersBloc bloc;
late MockGetAllCustomers mockGetAllCustomers;
late MockAddCustomer mockAddCustomer;
late MockUpdateCustomer mockUpdateCustomer;
late MockDeleteCustomer mockDeleteCustomer;
late MockDbHelper dbHelper;
late Database database;
CustomerEntity customer = CustomerEntity(
firstName: 'firstName',
lastName: 'lastName',
dateOfBirth: 'dateOfBirth',
phoneNumber: 'phoneNumber',
email: 'email',
bankAccountNumber: 'bankAccountNumber');
setUpAll(() async {
// Initialize FFI
sqfliteFfiInit();
database = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
await database.execute(
'CREATE TABLE $customersTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colFirstName TEXT, $colLastName TEXT, $colDateOfBirth TEXT, $colPhoneNumber TEXT, $colEmail TEXT, $colAccountNum TEXT)');
dbHelper = MockDbHelper();
dbHelper.database = database;
databaseFactory = databaseFactoryFfi;
});
setUp(() {
mockGetAllCustomers = MockGetAllCustomers();
mockAddCustomer = MockAddCustomer();
mockUpdateCustomer = MockUpdateCustomer();
mockDeleteCustomer = MockDeleteCustomer();
bloc = CustomersBloc(mockGetAllCustomers, mockAddCustomer,
mockUpdateCustomer, mockDeleteCustomer);
getIt.registerFactory(() => bloc);
});
group('''Add Feature''', () {
testWidgets('''Landing on the home screen''', (tester) async {
await theAppIsRunning(tester);
await iSeeEnabledElevatedButton(tester);
});
});
}
It's a part of the exception message I'm receiving:
type 'Null' is not a subtype of type 'Future<DataState<List<CustomerEntity>>>'
package:mc_crud_test/features/customer_feature/domain/usecases/get_all_customers_usecase.dart 11:43 MockGetAllCustomers.execute
package:mc_crud_test/features/customer_feature/presentation/bloc/bloc/customers_bloc.dart 43:60 new CustomersBloc.<fn>
package:bloc/src/bloc.dart 226:26 Bloc.on.<fn>.handleEvent
package:bloc/src/bloc.dart 235:9 Bloc.on.<fn>
dart:async
And it's my database class:
String customersTable = 'customers_table';
String colId = 'id';
String colFirstName = 'firstName';
String colLastName = 'lastName';
String colDateOfBirth = 'dateOfBirth';
String colPhoneNumber = 'phoneNumber';
String colEmail = 'email';
String colAccountNum = 'bankAccountNumber';
//
class DBHelper {
//
Database database;
DBHelper({required this.database});
//Database initialization and creation
static Future<Database> initDatabase() async {
//
final dbPath = await sql.getDatabasesPath();
return await sql.openDatabase(
path.join(dbPath, 'customers.db'),
onCreate: (db, version) {
return db.execute(
'CREATE TABLE $customersTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colFirstName TEXT, $colLastName TEXT, $colDateOfBirth TEXT, $colPhoneNumber TEXT, $colEmail TEXT, $colAccountNum TEXT)');
},
version: 1,
);
}
//
//find a customer by firstName, lastName, dateOfBirth, and email
Future<bool> findCustomer(CustomerEntity customer) async {
//
List<Map<String, dynamic>> map = await database.query(customersTable,
columns: [colFirstName, colLastName, colDateOfBirth, colEmail],
where:
'$colFirstName = ? OR $colLastName = ? OR $colDateOfBirth = ? OR $colEmail = ?',
whereArgs: [
customer.firstName,
customer.lastName,
customer.dateOfBirth,
customer.email
]);
if (map.isEmpty) {
return false;
} else {
return true;
}
}
// Add a customer to the Database
Future<int> insertCustomer(CustomerEntity customer) async {
//
final id = await database.insert(
customersTable,
customer.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
return id;
}
// Get the list of customers
Future<List<CustomerEntity>> getAllCustomers() async {
//
try {
final List<Map<String, Object?>> queryResult = await database.query(
customersTable,
orderBy: colId,
);
return queryResult.isEmpty
? []
: queryResult.map((e) => CustomerEntity.fromMapObject(e)).toList();
} catch (e) {
print('Error reading database : $e');
return [];
}
}
//
// Delete a Customer
Future<int> deleteCustomer(int id) async {
return await database
.delete(customersTable, where: '$colId = ?', whereArgs: [id]);
}
//
//Update a Customer
Future<bool> updateCustomer(CustomerEntity customer) async {
try {
final count = await database.update(customersTable, customer.toMap(),
where: '$colId = ?', whereArgs: [customer.id]);
if (count == 1) {
return true;
} else {
return false;
}
} catch (e) {
print('Failed to update the customer: $e');
return false;
}
}
}
Thos are the two function from the same class I wrote: The first has the error:
A value of type 'Future<String?>' can't be returned by the 'onError' handler because it must be assignable to 'FutureOr'.
while the second is OK and I don't understand why.
Can you tell me please?
/// Creates or update the User collection
Future<String?> _createOrUpdateUser(User user) {
final email = user.email;
final nameFromEmail =
email!.substring(0, email.indexOf('#')).replaceAll(".", " ").trim();
return FirebaseFirestore.instance.collection('users').doc(user.uid).set({
'id': user.uid,
'screenName': '',
'displayName': user.displayName ?? nameFromEmail,
'photoUrl': user.photoURL,
'bio': '',
'darkMode': false,
'email': user.email,
}).then((_) {
debugPrint("User ${user.displayName} is created");
return null;
}).catchError((e) {
debugPrint(e.toString());
return Future<String?>.error(e); // <<<<< Error here: A value of type 'Future<String?>' can't be returned by the 'onError' handler because it must be assignable to 'FutureOr<Null>'.
});
}
Future<String?> _authUser(BuildContext context, LoginData data) {
debugPrint('Name: ${data.name}, Password: ${data.password}');
return auth
.signInWithEmailAndPassword(email: data.name, password: data.password)
.then((credential) {
try {
final user = credential.user;
if (user == null) {
return 'Password does not match';
} else {
_createOrUpdateUser(user);
// == Tell the app we just signed in
Provider.of<StateModel>(context, listen: false).signedIn = user;
return null;
}
} catch (e) {
return 'User not exists';
}
}).catchError((e) {
if (e.code == 'weak-password') {
debugPrint('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
debugPrint('The account already exists for that email.');
} else {
debugPrint(e.toString());
}
return Future<String?>.error(e); // <<<<< No problem here
});
}
i use the following function to fetch userData from Firestore:
Future<String>getRegisterUserData({String userID}) async {
Firestore.instance.collection("Users").document(userID).get().then(
(datasnapshot) {
if (datasnapshot.exists) {
return datasnapshot.data['Email'];
} else {
return "Loading...";
}
},
);
}
I execute this function on my UserProfilePage like this:
_email = widget.firestore.getRegisterUserData(widget.userID).toString();
But i always get the print statement: Instance of 'Future' and not the saved email-address...
i also try this:
Future<String> getRegisterUserData({String userID}) async {
String email;
Firestore.instance.collection("Users").document(userID).get().then(
(datasnapshot) {
if (datasnapshot.exists) {
email = datasnapshot.data['Email'];
} else {
email = "Loading...";
}
},
);
return email;
}
The Print Statement is always the same...
So where is my mistake? I want to display the Value of 'Email' on the UserProfilePage in a variable, or is my firestore function incorrect?
Thank you for our help
Add await keyword. But at a different place.
tempEmail = await widget.firestore.getRegisterUserData(widget.userID);
setState(() {
_email = tempEmail;
});
// we don't need toString as getRegisterUserData itself is returning Future<String>
Note for using await: As we are using await the method/function which contains this should have a async in its signature.
Or you can use then block
widget.firestore.getRegisterUserData(widget.userID).then((email) {
setState(() {
_email = email;
});
});
Explanation: widget.firestore.getRegisterUserData(widget.userID) is of type Future<String>. that's why it is printed as Instance of 'Future'. We have to convert the Future to String by await or by then block
Using SharedPreferences:
Future<String> getEmail() async {
final prefs = await SharedPreferences.getInstance();
String email = prefs.getString('email');
if (email != null) {
return email;
} else {
email = await widget.firestore.getRegisterUserData(widget.userID);
prefs.setString('email', email); //update shared preferences
return email;
}
}
// usage (may be in initState)
widget.getEmail().then((email) {
setState(() {
_email = email;
})
})
Updated
Based on your information, you need a FutureBuilder in order to wait the response to build your widget:
return FutureBuilder(
future: getRegisterUserData(userID: "1234"),
builder: (context, asyncsnapshot){
return asyncsnapshot.hasData && asyncsnapshot.data != null ? TextFormField(
initialValue: asyncsnapshot.data,
) : CircularProgressIndicator();
}
);
I've set a custom unique validator decorator on my TypeORM entity field email. NestJS has dependency injection, but the service is not injected.
The error is:
TypeError: Cannot read property 'findByEmail' of undefined
Any help on implementing a custom email validator?
user.entity.ts:
#Column()
#Validate(CustomEmail, {
message: "Title is too short or long!"
})
#IsEmail()
email: string;
My CustomEmail validator is
import {ValidatorConstraint, ValidatorConstraintInterface,
ValidationArguments} from "class-validator";
import {UserService} from "./user.service";
#ValidatorConstraint({ name: "customText", async: true })
export class CustomEmail implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(text: string, args: ValidationArguments) {
const user = await this.userService.findByEmail(text);
return !user;
}
defaultMessage(args: ValidationArguments) {
return "Text ($value) is too short or too long!";
}
}
I know I could set unique in the Column options
#Column({
unique: true
})
but this throws a mysql error and the ExceptionsHandler that crashes my app, so I can't handle it myself...
Thankx!
I can propose 2 different approaches here, the first one catches the constraint violation error locally without additional request, and the second one uses a global error filter, catching such errors in the entire application. I personally use the latter.
Local no-db request solution
No need to make additional database request. You can catch the error violating the unique constraint and throw any HttpException you want to the client. In users.service.ts:
public create(newUser: Partial<UserEntity>): Promise<UserEntity> {
return this.usersRepository.save(newUser).catch((e) => {
if (/(email)[\s\S]+(already exists)/.test(e.detail)) {
throw new BadRequestException(
'Account with this email already exists.',
);
}
return e;
});
}
Which will return:
Global error filter solution
Or even create a global QueryErrorFilter:
#Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter {
public catch(exception: any, host: ArgumentsHost): any {
const detail = exception.detail;
if (typeof detail === 'string' && detail.includes('already exists')) {
const messageStart = exception.table.split('_').join(' ') + ' with';
throw new BadRequestException(
exception.detail.replace('Key', messageStart),
);
}
return super.catch(exception, host);
}
}
Then in main.ts:
async function bootstrap() {
const app = await NestFactory.create(/**/);
/* ... */
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
/* ... */
await app.listen(3000);
}
bootstrap();
This will give generic $table entity with ($field)=($value) already exists. error message. Example:
I have modified my code. I am checking the uniqueness of username/email in the user service (instead of a custom validator) and return an HttpExcetion in case the user is already inserted in the DB.
The easiest solution!
#Entity()
export class MyEntity extends BaseEntity{
#Column({unique:true}) name:string;
}
export abstract class BaseDataService<T> {
constructor(protected readonly repo: Repository<T>) {}
private async isUnique(t: any) {
const uniqueColumns = this.repo.metadata.uniques.map(
(e) => e.givenColumnNames[0]
);
for (const u of uniqueColumns) {
const count = await this.repo.count({ where: { [u]: ILike(t[u]) } });
if (count > 0) {
throw new UnprocessableEntityException(`${u} must be unique!`);
}
}
}
async save(body: DeepPartial<T>) {
await this.isUnique(body);
try {
return await this.repo.save(body);
} catch (err) {
throw new UnprocessableEntityException(err.message);
}
}
async update(id: number, updated: QueryDeepPartialEntity<T>) {
await this.isUnique(updated)
try {
return await this.repo.update(id, updated);
} catch (err) {
throw new UnprocessableEntityException(err.message);
}
}
}
An approach that works for modern version of NestJS which is based in Daniel Kucal's answer and actually returns the error to the frontend when calling the JSON API is the following:
import {
Catch,
ArgumentsHost,
BadRequestException,
HttpException,
} from '#nestjs/common';
import { BaseExceptionFilter } from '#nestjs/core';
import { QueryFailedError } from 'typeorm';
type ExceptionType = { detail: string; table: string };
#Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter<
HttpException | ExceptionType
> {
public catch(exception: ExceptionType, host: ArgumentsHost): void {
const { detail = null } = exception || {};
if (
!detail ||
typeof detail !== 'string' ||
// deepcode ignore AttrAccessOnNull: <False positive>
!detail.includes('already exists')
) {
return super.catch(exception, host);
} // else
/**
* this regex transform the message `(phone)=(123)` to a more intuitive `with phone: "123"` one,
* the regex is long to prevent mistakes if the value itself is ()=(), for example, (phone)=(()=())
*/
const extractMessageRegex =
/\((.*?)(?:(?:\)=\()(?!.*(\))(?!.*\))=\()(.*?)\)(?!.*\)))(?!.*(?:\)=\()(?!.*\)=\()((.*?)\))(?!.*\)))/;
const messageStart = `${exception.table.split('_').join(' ')} with`;
/** prevent Regex DoS, doesn't treat messages longer than 200 characters */
const exceptionDetail =
exception.detail.length <= 200
? exception.detail.replace(extractMessageRegex, 'with $1: "$3"')
: exception.detail;
super.catch(
new BadRequestException(exceptionDetail.replace('Key', messageStart)),
host,
);
}
}
Also, not forgetting main.ts:
async function bootstrap() {
const app = await NestFactory.create(/**/);
/* ... */
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
/* ... */
await app.listen(3000);
}
bootstrap();
The following code was posted as the new handling for openCursor. Could someone please advise if the following code should work now (Dart r18915), and what is the "values" variable?
store.openCursor(autoAdvance: true).listen(
(cursor) => values.add(onCursor(cursor)),
onDone: () => completer.complete(values),
onError: (e) => completer.completeError(e));
return completer.future;
I'm not sure what your onCursor() function is doing/calling. I'm assuming your values variable is a list.
I do something similar myself but with a callback instead of a future/completer. You only have a small function fragment here. I'm going to flush it out some to hopefully add some details:
// Accept the indexeddb Database, and return a future which
// will provide the list of values from 'someKey'
Future<List<String>> getSomeKeyValues(Database db) {
var values = new List<String>();
var completer = new Completer();
var store = db.transaction('DB_STORE', 'readwrite').objectStore('DB_STORE');
store.openCursor(autoAdvance: true).listen((cursor) {
if(cursor != null && cursor.value != null) {
// Note cursor.value is actually a Map<String, String>
// of collection's key and value
values.add(cursor.value['someKey']); // get the value key 'someKey'
}
}, onDone: () => completer.complete(values),
onError: (e) => completer.completeError(e));
return completer.future;
}
We would then call this function something like this:
getSomeKeyValues(myDatabase).then((results) {
for(var value in results) {
print(value);
}
}