Skip to content

Commit

Permalink
Merge pull request #48 from eficode/feature/verification-email-required
Browse files Browse the repository at this point in the history
Feature/verification email required
  • Loading branch information
Alb93 authored Jan 3, 2021
2 parents 0229c76 + 88c15f3 commit f8d6bec
Show file tree
Hide file tree
Showing 23 changed files with 243 additions and 71 deletions.
18 changes: 2 additions & 16 deletions packages/game-app/cypress/integration/auth_routing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import firebase from "firebase/app";

context("Routing", () => {
beforeEach(() => {
cy.clearLocalStorage()
cy.clearIndexedDB();
cy.visit(Cypress.config().baseUrl!);
});

Expand All @@ -23,20 +25,4 @@ context("Routing", () => {
cy.location("pathname").should("equal", "/signup");
});

it("should show Dashboard to authenticated user", () => {
cy.window().then((win) => {
((win as any).firebase as typeof firebase)
.auth()
.setPersistence(firebase.auth.Auth.Persistence.NONE)
.then((_) => {
((win as any).firebase as typeof firebase)
.auth()
.createUserWithEmailAndPassword("test@test.test", "t3st-u53r-111!")
.then(() => {
cy.visit("/dashboard");
cy.location("pathname").should("equal", "/dashboard");
});
});
});
});
});
42 changes: 42 additions & 0 deletions packages/game-app/cypress/integration/email_verification.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/// <reference types="Cypress" />
/// <reference types="../support" />

// @ts-ignore
context("Email verification", () => {

beforeEach(() => {
cy.clearLocalStorage()
cy.clearIndexedDB();
cy.visit(Cypress.config().baseUrl!);
});

it("should show verification email required message", () => {
const randomEmail = `testEmail${Math.floor(Math.random() * 1000)}@email.com`.toLocaleLowerCase();
cy.window().its('store').invoke('dispatch', {
type: 'signup/start', payload: {
email:randomEmail,
password:'Aa1%sfesfsf',
repeatPassword:'Aa1%sfesfsf',
role:'endUser',
devOpsMaturity:'veryImmature',
}
});
cy.get('body').should('contain.translationOf', 'signup.verificationRequired.message');
});

it("should resend verification email correctly", () => {
const randomEmail = `testEmail${Math.floor(Math.random() * 1000)}@email.com`.toLocaleLowerCase();
cy.window().its('store').invoke('dispatch', {
type: 'signup/start', payload: {
email:randomEmail,
password:'Aa1%sfesfsf',
repeatPassword:'Aa1%sfesfsf',
role:'endUser',
devOpsMaturity:'veryImmature',
}
});
cy.containsTranslationOf('signup.verificationRequired.resend').click();
cy.get('body').should('contain.translationOf', 'signup.verificationRequired.resendSuccess');
});

});
17 changes: 9 additions & 8 deletions packages/game-app/cypress/integration/signup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ context("Signup", () => {
const usedEmails: string[] = [];

beforeEach(() => {
cy.clearLocalStorage()
cy.clearIndexedDB();
cy.visit(Cypress.config().baseUrl!);
});

Expand All @@ -24,7 +26,7 @@ context("Signup", () => {
});

it("should not show invalid password error for correct password", () => {
cy.getInputByName('password').fill('Aa1%sfesfsf');
cy.getInputByName('password').fill('Aa1sfesfsf');
cy.get('button').containsTranslationOf('signup.form.buttonText').click();
cy.get('body').should('not.contain.translationOf', 'signup.errors.passwordRequirements')
});
Expand All @@ -36,17 +38,16 @@ context("Signup", () => {
cy.get('body').should('contain.translationOf', 'signup.errors.passwordMatch')
});

it("should signup correctly", () => {
it("should signup correctly and go to email verification required", () => {
const randomEmail = `testEmail${Math.floor(Math.random() * 1000)}@email.com`.toLocaleLowerCase();
usedEmails.push(randomEmail);
cy.getInputByName('email').fill(randomEmail);
cy.getInputByName('password').fill('Aa1%sfesfsf');
cy.getInputByName('repeatPassword').fill('Aa1%sfesfsf');
cy.getInputByName('password').fill('Aa1sfesfsf');
cy.getInputByName('repeatPassword').fill('Aa1sfesfsf');
cy.getInputByName('role').select('endUser');
cy.getInputByName('devOpsMaturity').select('veryImmature');
cy.get('button').containsTranslationOf('signup.form.buttonText').click();
cy.get('body').should('contain', 'Loading');
cy.get('body').should('contain', 'Success');
cy.location("pathname").should("equal", "/email-verification-required");

// check auth presence
cy.getFirebaseUserByEmail(randomEmail).should('deep.include', {
Expand All @@ -66,8 +67,8 @@ context("Signup", () => {
it("should show email already used error", () => {
const alreadyUsedEmail = usedEmails[0];
cy.getInputByName('email').fill(alreadyUsedEmail);
cy.getInputByName('password').fill('Aa1%sfesfsf');
cy.getInputByName('repeatPassword').fill('Aa1%sfesfsf');
cy.getInputByName('password').fill('Aa1sfesfsf');
cy.getInputByName('repeatPassword').fill('Aa1sfesfsf');
cy.getInputByName('role').select('endUser');
cy.getInputByName('devOpsMaturity').select('veryImmature');
cy.get('button').containsTranslationOf('signup.form.buttonText').click();
Expand Down
46 changes: 24 additions & 22 deletions packages/game-app/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,17 @@
/// <reference types="Cypress" />

// @ts-ignore
Cypress.Commands.add("containsTranslationOf", {prevSubject: true}, (subject, key: string) => {
Cypress.Commands.add("containsTranslationOf", {prevSubject: 'optional'}, (subject, key: string) => {
cy.window({log: false}).then((win) => {
cy.wrap(subject, {log: false}).contains((win as any).i18n.t(key));
if (subject) {
cy.wrap(subject, {log: false}).contains((win as any).i18n.t(key));
} else {
cy.contains((win as any).i18n.t(key));
}
});
});


Cypress.Commands.overwrite("should", (originalFn, url, condition, param) => {
if (condition === 'contain.translationOf') {
return originalFn(url, (el$: any) => {
cy.window({log: false}).then((win) => {
console.debug((win as any).i18n.t(param));
expect(el$).contain((win as any).i18n.t(param))
});
});
} else if (condition === 'not.contain.translationOf') {
return originalFn(url, (el$: any) => {
cy.window({log: false}).then((win) => {
console.debug((win as any).i18n.t(param));
expect(el$).not.contain((win as any).i18n.t(param))
});
});
} else {
return originalFn(url, condition, param);
}
})

Cypress.Commands.add('getInputByName', (name: string, options: Parameters<typeof cy.get>[1]) => {
cy.get(`input[name="${name}"],select[name="${name}"]`, options);
});
Expand Down Expand Up @@ -105,3 +89,21 @@ Cypress.Commands.add('fill', {prevSubject: 'element'}, (subject, value) => {

}
)

Cypress.Commands.add('clearIndexedDB', async () => {
const databases = await (window.indexedDB as any).databases();

await Promise.all(
databases.map(({name}: any) =>
new Promise((resolve, reject) => {
const request = window.indexedDB.deleteDatabase(name);

request.addEventListener('success', resolve);
// Note: we need to also listen to the "blocked" event
// (and resolve the promise) due to https://stackoverflow.com/a/35141818
request.addEventListener('blocked', resolve);
request.addEventListener('error', reject);
}),
),
);
});
21 changes: 21 additions & 0 deletions packages/game-app/cypress/support/customAssertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Chai assertion for containing translated string
*/
chai.Assertion.addMethod('translationOf', function (key: string) {
const $element = this._obj;
const element = $element[0];

const doc = element.ownerDocument;
const win = doc.defaultView || doc.parentWindow;

const translation = win.i18n.t(key);

const res = $element.text().indexOf(win.i18n.t(key)) !== -1;

this.assert(
res
, "expected #{this} to contain #{exp}"
, "expected #{this} to not contain #{exp}"
, translation // expected
);
});
5 changes: 5 additions & 0 deletions packages/game-app/cypress/support/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ declare namespace Cypress {
getFirebaseUserByEmail(email: string): Chainable<any>;

getFirestoreDocument(path: string): Chainable<any>;
/**
* Delete all data in the indexedDB, useful for example to delete all
* firebase authentication persisted data
*/
clearIndexedDB(): Chainable<any>;

/**
* Insert value in input without typing effect and delay.
Expand Down
3 changes: 3 additions & 0 deletions packages/game-app/cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
import './commands'
import '@cypress/code-coverage/support'
import './customAssertions'


15 changes: 14 additions & 1 deletion packages/game-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import React, { Suspense } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { PrivateRoute, RoutingPath } from '@pipeline/routing';
import { useBootstrapIsFinished } from './_shared';
import { useLoggedUser } from '@pipeline/auth';

const Signup = React.lazy(() => import('./signup/components/Signup'));
const EmailVerificationRequired = React.lazy(() => import('./signup/components/EmailVerificationRequired'));

function App() {
const bootstrapIsFinished = useBootstrapIsFinished();

const user = useLoggedUser();

const { pathname } = useLocation();

return bootstrapIsFinished ? (
<Suspense fallback={null}>
<Switch>
{user && !user.emailVerified && pathname !== RoutingPath.EmailVerificationRequired ? (
<Route path="*">
<Redirect to={RoutingPath.EmailVerificationRequired} />
</Route>
) : null}
<Route path={RoutingPath.Login} render={() => <div>Login</div>} />
<Route path={RoutingPath.Signup} component={Signup} />
<Route path={RoutingPath.EmailVerificationRequired} component={EmailVerificationRequired} />
<Route path={RoutingPath.VerifyEmail} component={() => <div>VerifyEmail</div>} />
<PrivateRoute path={RoutingPath.Dashboard} render={() => <div>Dashboard</div>} />
<Route path="*">
<Redirect to={RoutingPath.Signup} />
Expand Down
7 changes: 7 additions & 0 deletions packages/game-app/src/_shared/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createRequestHook } from '@pipeline/requests-status';
import { actions } from './slice';

export const useResendVerificationEmail = createRequestHook(
'auth.resendVerificationEmail',
actions.resendEmailVerification,
);
8 changes: 6 additions & 2 deletions packages/game-app/src/_shared/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { reducer, actions, name, selectors } from './slice';
import { reducer, actions, name, selectors, AuthUser } from './slice';
import saga from './saga';
import useLoggedUser from './useLoggedUser';
import { useResendVerificationEmail } from './hooks';

export { reducer, actions, name, saga, selectors };
export { reducer, actions, name, saga, selectors, useLoggedUser, useResendVerificationEmail };

export type { AuthUser };
18 changes: 14 additions & 4 deletions packages/game-app/src/_shared/auth/saga.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { actions, User } from './slice';
import { actions, AuthUser } from './slice';
import firebase from 'firebase/app';
import 'firebase/auth';
import { addRequestStatusManagement } from '@pipeline/requests-status';

function getCurrentUser(): Promise<User | null> {
return new Promise<User | null>(resolve => {
function getCurrentUser(): Promise<AuthUser | null> {
return new Promise<AuthUser | null>(resolve => {
firebase.auth().onAuthStateChanged(user => {
if (user) {
resolve({
id: user.uid,
email: user.email!,
emailVerified: user.emailVerified,
});
} else {
resolve(null);
Expand All @@ -19,10 +21,18 @@ function getCurrentUser(): Promise<User | null> {
}

function* initializeAuthSaga() {
const user: User | null = yield call(getCurrentUser);
const user: AuthUser | null = yield call(getCurrentUser);
yield put(actions.setLoggedUser(user));
}

function* resendVerificationEmail() {
yield call(() => firebase.auth().currentUser?.sendEmailVerification());
}

export default function* authSaga() {
yield takeEvery(actions.initialize, initializeAuthSaga);
yield takeEvery(
actions.resendEmailVerification,
addRequestStatusManagement(resendVerificationEmail, 'auth.resendVerificationEmail'),
);
}
15 changes: 10 additions & 5 deletions packages/game-app/src/_shared/auth/slice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface User {
export interface AuthUser {
id: string;
email: string;
emailVerified: boolean;
}

export interface State {
isInitialized: boolean;
loggedUser: User | null;
loggedUser: AuthUser | null;
}

const initialState = {
Expand All @@ -19,7 +20,7 @@ const slice = createSlice({
name: 'auth',
initialState: initialState,
reducers: {
setLoggedUser(state, action: PayloadAction<User | null>) {
setLoggedUser(state, action: PayloadAction<AuthUser | null>) {
state.isInitialized = true;
state.loggedUser = action.payload;
},
Expand All @@ -41,8 +42,12 @@ const isInitialized = createSelector(
);

export const reducer = slice.reducer;
export const actions = slice.actions;
export const name = slice.name;

export const actions = {
...slice.actions,
resendEmailVerification: createAction(`${name}/resendEmailVerification`),
};
export const selectors = {
getCurrentUser,
isInitialized,
Expand Down
10 changes: 10 additions & 0 deletions packages/game-app/src/_shared/auth/useLoggedUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useSelector } from 'react-redux';
import { selectors } from './slice';

/**
* Get current logged user info. null if not logged.
*/
export default function useLoggedUser() {
const loggedUser = useSelector(selectors.getCurrentUser);
return loggedUser;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface RequestsKeys {
signup: null;
gameRoles: null;
devOpsMaturities: null;
'auth.resendVerificationEmail': null;
}
3 changes: 2 additions & 1 deletion packages/game-app/src/_shared/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RoutingPath } from './routingPath';
import PrivateRoute from './PrivateRoute';
import useNavigateOnCondition from './useNavigateOnCondition';

export { RoutingPath, PrivateRoute };
export { RoutingPath, PrivateRoute, useNavigateOnCondition };
Loading

0 comments on commit f8d6bec

Please sign in to comment.