Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication service along with user module #18

Merged
merged 8 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
REACT_APP_API_KEY=FIREBASE_API_KEY
REACT_APP_AUTH_DOMAIN=FIREBASE_AUTH_DOMAIN
REACT_APP_PROJECT_ID=FIREBASE_PROJECT_ID
REACT_APP_STORAGE_BUCKET=FIREBASE_STORAGE_BUCKET
REACT_APP_MESSAGING_SENDER_ID=FIREBASE_MESSAGING_SENDER_ID
REACT_APP_APP_ID=FIREBASE_APP_ID
REACT_APP_MEASUREMENT_ID=FIREBASE_MEASUREMENT_ID
8 changes: 8 additions & 0 deletions .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ jobs:

- name: Create build
run: yarn run build
env:
REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }}
REACT_APP_AUTH_DOMAIN: ${{ secrets.REACT_APP_AUTH_DOMAIN }}
REACT_APP_PROJECT_ID: ${{ secrets.REACT_APP_PROJECT_ID }}
REACT_APP_STORAGE_BUCKET: ${{ secrets.REACT_APP_STORAGE_BUCKET }}
REACT_APP_MESSAGING_SENDER_ID: ${{ secrets.REACT_APP_MESSAGING_SENDER_ID }}
REACT_APP_APP_ID: ${{ secrets.REACT_APP_APP_ID }}
REACT_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }}

# Deploy to Netlify
- name: Deploy draft to Netlify
Expand Down
14 changes: 11 additions & 3 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ jobs:
run: yarn install

- name: Test linting
run: yarn lint
run: yarn lint

- name: Run test cases and coverage
run: yarn test:coverage
run: yarn test:coverage

- name: Create build
run: yarn run build
env:
REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }}
REACT_APP_AUTH_DOMAIN: ${{ secrets.REACT_APP_AUTH_DOMAIN }}
REACT_APP_PROJECT_ID: ${{ secrets.REACT_APP_PROJECT_ID }}
REACT_APP_STORAGE_BUCKET: ${{ secrets.REACT_APP_STORAGE_BUCKET }}
REACT_APP_MESSAGING_SENDER_ID: ${{ secrets.REACT_APP_MESSAGING_SENDER_ID }}
REACT_APP_APP_ID: ${{ secrets.REACT_APP_APP_ID }}
REACT_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }}

# Deploy to Netlify
- name: Deploy production to Netlify
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,10 @@ Lowercase is preferred for file and folder names, one exception to this is Files
- Make sure you cover at least 70% test-case scenario.
- You can generate "test coverage" for modified files using `yarn test:coverage` command.
- if you want to test cases for any particular file you can add path along with command for e.g.`yarn test src/Home/Home.test.tsx`

### Environment Variables

- You can create your own firebase project and add required `.env` variables.
- Check `.env-example` to see which firebase keys are needed.
- REACT_APP prefix is mandatory in `CRA` to declare environment variable.
- More details on adding custom environment variable in `CRA` can be found at [Adding custom environment variables(creat-react-app)](https://create-react-app.dev/docs/adding-custom-environment-variables/)
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@types/node": "^12.19.11",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"firebase": "^8.6.5",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
Expand Down Expand Up @@ -52,7 +53,9 @@
"devDependencies": {
"@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.9",
"cross-env": "^7.0.3",
"firebase-admin": "^9.9.0",
"firebase-functions": "^3.14.1",
codeAesthetic marked this conversation as resolved.
Show resolved Hide resolved
"firebase-functions-test": "^0.3.0",
"husky": "1.0.0-rc.13"
}
}
7 changes: 6 additions & 1 deletion src/common/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ export default interface IRoute {

export interface IParams {
id?: string | undefined;
}
}

export interface AuthFormData {
email: string;
password: string;
}
50 changes: 50 additions & 0 deletions src/modules/AuthService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as AuthService from "src/modules/AuthService";
import { User } from "src/modules/User";

jest.mock("firebase", () => ({
initializeApp: jest.fn(() => {
const functions = require("firebase-functions-test");
const testEnv = functions();
const firebaseUser = testEnv.auth.exampleUserRecord();
return {
auth: () => {
return {
signInWithEmailAndPassword: () =>
Promise.resolve({ user: firebaseUser }),
Comment on lines +12 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to mock these functions as per: https://jestjs.io/docs/mock-functions#using-a-mock-function
So that we can check if the same was called in the respective wrapper with a check like:

// The first argument of the first call to the function was 0
expect(signInWithEmialAndPassword.mock.calls[0][0]).toBe(0);

Copy link
Contributor Author

@codeAesthetic codeAesthetic Jun 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function that lies inside another function can't be mocked as you mentioned.
more details at following:-

  1. Spying on an imported function that calls another function in Jest,
  2. jest mock inner functions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two examples are different from our case. Can you try something like:

const mockSignInWithEmailAndPassword = jest.fn(({email, password}) =>  Promise.resolve({ user: firebaseUser }));

jest.mock("firebase", () => ({
  ...
  return {
    auth: () => ({
        signInWithEmailAndPassword,
        ...
     })
  }
});

....

expect(mockSignInWithEmailAndPassword.mock.calls[0][0]).toBe("user@gmail.com");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mockSignInWithEmailAndPassword is simple mock function it can't test AuthService.ts file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this on a codesandbox with the above approach or share some references

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example you have given doesn't work so don't see the point of putting this in sandbox

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codesandbox would allow me to explain the mocking. Can you create one and post the link here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jest is not fully supported in codesandbox: codesandbox/codesandbox-client#513

createUserWithEmailAndPassword: () =>
Promise.resolve({ user: firebaseUser }),
sendPasswordResetEmail: () => Promise.resolve(),
signOut: () => Promise.resolve(),
};
},
};
}),
}));

describe("Auth Service", () => {
chirgjn marked this conversation as resolved.
Show resolved Hide resolved
it("Should Sign In", async () => {
const existingUser: User = await AuthService.signIn({
email: "user@gmail.com",
password: "******",
});
expect(existingUser).toHaveProperty("user.email", "user@gmail.com");
});

it("Should Sign Up", async () => {
const newUser: User = await AuthService.signUp({
email: "user@gmail.com",
password: "******",
});
expect(newUser).toHaveProperty("user.email", "user@gmail.com");
});

it("Should request password reset", async () => {
const result = await AuthService.requestPasswordReset("user@gmail.com");
expect(result).toEqual(undefined);
});

it("Should Sign out user", async () => {
const result = await AuthService.signOut();
expect(result).toEqual(undefined);
});
});
54 changes: 54 additions & 0 deletions src/modules/AuthService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import firebase from "firebase";
import { AuthFormData } from "src/common/interface";
import { User } from "src/modules/User";

const {
REACT_APP_API_KEY,
REACT_APP_AUTH_DOMAIN,
REACT_APP_PROJECT_ID,
REACT_APP_STORAGE_BUCKET,
REACT_APP_MESSAGING_SENDER_ID,
REACT_APP_APP_ID,
REACT_APP_MEASUREMENT_ID,
} = process.env;

const firebaseConfig = {
apiKey: REACT_APP_API_KEY,
authDomain: REACT_APP_AUTH_DOMAIN,
projectId: REACT_APP_PROJECT_ID,
storageBucket: REACT_APP_STORAGE_BUCKET,
messagingSenderId: REACT_APP_MESSAGING_SENDER_ID,
appId: REACT_APP_APP_ID,
measurementId: REACT_APP_MEASUREMENT_ID,
};

const firebaseApp = firebase.initializeApp(firebaseConfig);
const auth = firebaseApp.auth();

export function signUp({ email, password }: AuthFormData): Promise<User> {
return auth
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
if (user === null) {
throw new Error("sign-up failure");
}
return new User(user);
});
}

export function signIn({ email, password }: AuthFormData): Promise<User> {
return auth.signInWithEmailAndPassword(email, password).then(({ user }) => {
if (user === null) {
throw new Error("sign-in failure");
}
return new User(user);
});
}

export function requestPasswordReset(email: string): Promise<void> {
return auth.sendPasswordResetEmail(email);
}

export function signOut(): Promise<void> {
return auth.signOut();
}
27 changes: 27 additions & 0 deletions src/modules/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import firebase from "firebase";

interface Profile {
email: string;
}

export class User {
private user: firebase.User;

constructor(user: firebase.User) {
this.user = user;
}

// getProfile will return email as of now
getProfile(): Profile {
if (this.user.email === null) {
throw new Error("email not found");
}
return { email: this.user.email };
}
/**
* Returns a JSON Web Token (JWT) used to identify the user to a Firebase service.
*/
getToken(): Promise<string> {
return this.user.getIdToken();
}
}
Loading