Skip to content

Commit

Permalink
Starter project for section 10
Browse files Browse the repository at this point in the history
  • Loading branch information
bizz84 committed May 15, 2024
1 parent 54773a7 commit 5d0948a
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 27 deletions.
2 changes: 1 addition & 1 deletion ecommerce_app/integration_test/purchase_flow_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() {
r.cart.expectFindNCartItems(1);
// checkout
await r.checkout.startCheckout();
await r.auth.signInWithEmailAndPassword();
await r.auth.enterAndSubmitEmailAndPassword();
r.cart.expectFindNCartItems(1);
await r.checkout.startPayment();
// when a payment is complete, user is taken to the orders page
Expand Down
50 changes: 50 additions & 0 deletions ecommerce_app/lib/src/exceptions/app_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:ecommerce_app/src/localization/string_hardcoded.dart';

/// Base class for all all client-side errors that can be generated by the app
sealed class AppException implements Exception {
AppException(this.code, this.message);
final String code;
final String message;
}

/// Auth
class EmailAlreadyInUseException extends AppException {
EmailAlreadyInUseException()
: super('email-already-in-use', 'Email already in use'.hardcoded);
}

class WeakPasswordException extends AppException {
WeakPasswordException()
: super('weak-password', 'Password is too weak'.hardcoded);
}

class WrongPasswordException extends AppException {
WrongPasswordException()
: super('wrong-password', 'Wrong password'.hardcoded);
}

class UserNotFoundException extends AppException {
UserNotFoundException() : super('user-not-found', 'User not found'.hardcoded);
}

/// Cart
class CartSyncFailedException extends AppException {
CartSyncFailedException()
: super('cart-sync-failed',
'An error has occurred while updating the shopping cart'.hardcoded);
}

/// Checkout
class PaymentFailureEmptyCartException extends AppException {
PaymentFailureEmptyCartException()
: super('payment-failure-empty-cart',
'Can\'t place an order if the cart is empty'.hardcoded);
}

/// Orders
class ParseOrderFailureException extends AppException {
ParseOrderFailureException(this.status)
: super('parse-order-failure',
'Could not parse order status: $status'.hardcoded);
final String status;
}
51 changes: 51 additions & 0 deletions ecommerce_app/lib/src/exceptions/app_exception_enum.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:ecommerce_app/src/localization/string_hardcoded.dart';

/// An exception type to represent all client-side errors that can be generated
/// by the app
enum AppExceptionEnum {
// Auth
emailAlreadyInUse('email-already-in-use'),
weakPassword('weak-password'),
wrongPassword('wrong-password'),
userNotFound('user-not-found'),
// Cart
cartSyncFailed('cart-sync-failed'),
// Checkout
paymentFailureEmptyCart('payment-failure-empty-cart'),
// Orders
parseOrderFailure('parse-order-failure');

const AppExceptionEnum(this.code);

/// A value that can be sent to the backend when logging the error
final String code;

/// A user-friendly message that can be shown in the UI.
// * This needs to be a getter variable or a method since the error message
// * can't be declared as const if it's localized
String get message {
switch (this) {
// Auth
case emailAlreadyInUse:
return 'Email already in use'.hardcoded;
case weakPassword:
return 'Password is too weak'.hardcoded;
case wrongPassword:
return 'Wrong password'.hardcoded;
case userNotFound:
return 'User not found'.hardcoded;
// Cart
case cartSyncFailed:
return 'An error has occurred while updating the shopping cart'
.hardcoded;
// Checkout
case paymentFailureEmptyCart:
return 'Can\'t place an order if the cart is empty'.hardcoded;
// Orders
case parseOrderFailure:
return 'Could not parse order status'.hardcoded;
default:
return 'Unknown error'.hardcoded;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:ecommerce_app/src/features/authentication/domain/app_user.dart';
import 'package:ecommerce_app/src/features/authentication/domain/fake_app_user.dart';
import 'package:ecommerce_app/src/localization/string_hardcoded.dart';
import 'package:ecommerce_app/src/utils/delay.dart';
import 'package:ecommerce_app/src/utils/in_memory_store.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -11,15 +13,41 @@ class FakeAuthRepository {
Stream<AppUser?> authStateChanges() => _authState.stream;
AppUser? get currentUser => _authState.value;

// List to keep track of all user accounts
final List<FakeAppUser> _users = [];

Future<void> signInWithEmailAndPassword(String email, String password) async {
await delay(addDelay);
_createNewUser(email);
// check the given credentials agains each registered user
for (final u in _users) {
// matching email and password
if (u.email == email && u.password == password) {
_authState.value = u;
return;
}
// same email, wrong password
if (u.email == email && u.password != password) {
throw Exception('Wrong password'.hardcoded);
}
}
throw Exception('User not found'.hardcoded);
}

Future<void> createUserWithEmailAndPassword(
String email, String password) async {
await delay(addDelay);
_createNewUser(email);
// check if the email is already in use
for (final u in _users) {
if (u.email == email) {
throw Exception('Email already in use'.hardcoded);
}
}
// minimum password length requirement
if (password.length < 8) {
throw Exception('Password is too weak'.hardcoded);
}
// create new user
_createNewUser(email, password);
}

Future<void> signOut() async {
Expand All @@ -28,11 +56,17 @@ class FakeAuthRepository {

void dispose() => _authState.close();

void _createNewUser(String email) {
_authState.value = AppUser(
void _createNewUser(String email, String password) {
// create new user
final user = FakeAppUser(
uid: email.split('').reversed.join(),
email: email,
password: password,
);
// register it
_users.add(user);
// update the auth state
_authState.value = user;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
class AppUser {
const AppUser({
required this.uid,
this.email,
required this.email,
});
final String uid;
final String? email;
final String email;

@override
bool operator ==(Object other) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:ecommerce_app/src/features/authentication/domain/app_user.dart';

/// Fake user class used to simulate a user account on the backend
class FakeAppUser extends AppUser {
FakeAppUser({
required super.uid,
required super.email,
required this.password,
});
final String password;
}
4 changes: 2 additions & 2 deletions ecommerce_app/lib/src/features/orders/domain/order.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:ecommerce_app/src/localization/string_hardcoded.dart';
import 'package:ecommerce_app/src/exceptions/app_exception.dart';

/// Order status
enum OrderStatus { confirmed, shipped, delivered }
Expand All @@ -9,7 +9,7 @@ extension OrderStatusString on OrderStatus {
if (string == 'confirmed') return OrderStatus.confirmed;
if (string == 'shipped') return OrderStatus.shipped;
if (string == 'delivered') return OrderStatus.delivered;
throw Exception('Could not parse order status: $string'.hardcoded);
throw ParseOrderFailureException(string);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ void main() {
r.products.expectFindAllProductCards();
await r.openPopupMenu();
await r.auth.openEmailPasswordSignInScreen();
await r.auth.signInWithEmailAndPassword();
await r.auth.tapFormToggleButton();
await r.auth.enterAndSubmitEmailAndPassword();
r.products.expectFindAllProductCards();
await r.openPopupMenu();
await r.auth.openAccountScreen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class AuthRobot {
expect(dialogTitle, findsNothing);
}

Future<void> signInWithEmailAndPassword() async {
Future<void> enterAndSubmitEmailAndPassword() async {
await enterEmail('test@test.com');
await tester.pump();
await enterPassword('test1234');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';

void main() {
const testEmail = 'test@test.com';
const testPassword = '1234';
const testPassword = 'test1234';
final testUser = AppUser(
uid: testEmail.split('').reversed.join(),
email: testEmail,
Expand All @@ -19,15 +19,19 @@ void main() {
expect(authRepository.currentUser, null);
expect(authRepository.authStateChanges(), emits(null));
});
test('currentUser is not null after sign in', () async {

test('sign in throws when user not found', () async {
final authRepository = makeAuthRepository();
addTearDown(authRepository.dispose);
await authRepository.signInWithEmailAndPassword(
testEmail,
testPassword,
await expectLater(
() => authRepository.signInWithEmailAndPassword(
testEmail,
testPassword,
),
throwsA(isA<Exception>()),
);
expect(authRepository.currentUser, testUser);
expect(authRepository.authStateChanges(), emits(testUser));
expect(authRepository.currentUser, null);
expect(authRepository.authStateChanges(), emits(null));
});

test('currentUser is not null after registration', () async {
Expand All @@ -44,7 +48,7 @@ void main() {
test('currentUser is null after sign out', () async {
final authRepository = makeAuthRepository();
addTearDown(authRepository.dispose);
await authRepository.signInWithEmailAndPassword(
await authRepository.createUserWithEmailAndPassword(
testEmail,
testPassword,
);
Expand All @@ -56,11 +60,11 @@ void main() {
expect(authRepository.authStateChanges(), emits(null));
});

test('sign in after dispose throws exception', () {
test('create user after dispose throws exception', () {
final authRepository = makeAuthRepository();
authRepository.dispose();
expect(
() => authRepository.signInWithEmailAndPassword(
() => authRepository.createUserWithEmailAndPassword(
testEmail,
testPassword,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ void main() {
setUpAll(() {
registerFallbackValue(const Cart());
});
const testUser = AppUser(uid: 'abc');
const testUser = AppUser(uid: 'abc', email: 'abc@test.com');

late MockAuthRepository authRepository;
late MockRemoteCartRepository remoteCartRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:mocktail/mocktail.dart';
import '../../../mocks.dart';

void main() {
const testUser = AppUser(uid: 'abc');
const testUser = AppUser(uid: 'abc', email: 'abc@test.com');
setUpAll(() {
// needed for MockOrdersRepository
registerFallbackValue(Order(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ void main() {
await r.checkout.startCheckout();
// sign in from checkout screen
r.auth.expectEmailAndPasswordFieldsFound();
await r.auth.signInWithEmailAndPassword();
await r.auth.enterAndSubmitEmailAndPassword();
// check that we move to the payment page
r.checkout.expectPayButtonFound();
});

testWidgets('checkout when previously signed in', (tester) async {
final r = Robot(tester);
await r.pumpMyApp();
// sign in first
// create an account first
await r.auth.openEmailPasswordSignInScreen();
await r.auth.signInWithEmailAndPassword();
await r.auth.tapFormToggleButton();
await r.auth.enterAndSubmitEmailAndPassword();
// then add a product and start checkout
await r.products.selectProduct();
await r.cart.addToCart();
Expand Down
2 changes: 1 addition & 1 deletion ecommerce_app/test/src/features/purchase_flow_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ void main() {
r.cart.expectFindNCartItems(1);
// checkout
await r.checkout.startCheckout();
await r.auth.signInWithEmailAndPassword();
await r.auth.enterAndSubmitEmailAndPassword();
r.cart.expectFindNCartItems(1);
await r.checkout.startPayment();
// when a payment is complete, user is taken to the orders page
Expand Down

0 comments on commit 5d0948a

Please sign in to comment.