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

Use auth0 #599

Merged
merged 4 commits into from
Feb 6, 2024
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
1 change: 0 additions & 1 deletion amplify/backend/function/stockerlambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"body-parser": "^1.17.1",
"cors": "^2.8.5",
"express": "^4.17.3",
"googleapis": "^131.0.0",
"luxon": "^3.3.4"
},
"scripts": {
Expand Down
44 changes: 0 additions & 44 deletions amplify/backend/function/stockerlambda/ts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import cors from 'cors';
import express from 'express';
import bodyParser from 'body-parser';
import awsServerlessExpressMiddleware from 'aws-serverless-express/middleware';
import { Auth } from 'googleapis';

import * as yahoo from './yahoo';
import { Price } from './types';
Expand Down Expand Up @@ -31,49 +30,6 @@ app.use(cors({
}
}));

app.get('/user/authorize', async (req, res) => {
const code = req.query.code as string;
if (!code) {
res.status(400).json({
error: 'CODE_REQUIRED',
description: 'Code is required for this endpoint',
});
}

const oauth2Client = new Auth.OAuth2Client(
'123339406534-gnk10bh5hqo87qlla8e9gmol1j961rtg.apps.googleusercontent.com',
process.env.CLIENT_SECRET,
'postmessage'
);

const response = await oauth2Client.getToken(code);

res.json(response.tokens);
});

app.get('/user/refresh', async (req, res) => {
const refresh_token = req.query.refresh_token as string;
if (!refresh_token) {
res.status(400).json({
error: 'REFRESH_TOKEN_REQUIRED',
description: 'Refresh token is required for this endpoint',
});
}

const oauth2Client = new Auth.OAuth2Client(
'123339406534-gnk10bh5hqo87qlla8e9gmol1j961rtg.apps.googleusercontent.com',
process.env.CLIENT_SECRET,
'postmessage'
);

oauth2Client.setCredentials({
refresh_token,
});
const response = await oauth2Client.refreshAccessToken();

res.json(response.credentials);
});

app.get('/api/prices', async (req, res) => {
let tickers = (req.query.ids as string || '').split(',');
if (!tickers.length) {
Expand Down
4 changes: 2 additions & 2 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const config = {
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
coverageThreshold: {
global: {
lines: 92.9,
branches: 86.6,
lines: 92,
branches: 85,
},
},
testEnvironment: 'jest-environment-jsdom',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"stocker:test:events": "yarn workspace stockerlambda test:events"
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
"@dinero.js/currencies": "^2.0.0-alpha.14",
"@fontsource/inter": "^5.0.16",
"@hookform/resolvers": "^3.3.4",
Expand Down
108 changes: 31 additions & 77 deletions src/__tests__/app/user/login/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import * as navigation from 'next/navigation';
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import * as auth0 from '@auth0/auth0-react';

import * as Stocker from '@/lib/Stocker';
import LoginPage from '@/app/user/login/page';
import * as helpers_env from '@/helpers/env';
import * as sessionHook from '@/hooks/useSession';
import type { Credentials } from '@/types/user';

jest.mock('swr');

jest.mock('next/navigation');

jest.mock('@auth0/auth0-react', () => ({
__esModule: true,
...jest.requireActual('@auth0/auth0-react'),
}));

jest.mock('@/lib/Stocker', () => ({
__esModule: true,
...jest.requireActual('@/lib/Stocker'),
Expand All @@ -23,101 +26,52 @@ jest.mock('@/helpers/env', () => ({
isStaging: () => false,
}));

jest.mock('@/hooks/useSession', () => ({
__esModule: true,
...jest.requireActual('@/hooks/useSession'),
}));

describe('LoginPage', () => {
let requestCode: jest.Mock;
let mockInitCodeClient: jest.Mock;
let mockSetCredentials: jest.Mock<React.Dispatch<React.SetStateAction<Credentials>>>;
let mockRouterPush: jest.Mock;

beforeEach(() => {
requestCode = jest.fn();
mockInitCodeClient = jest.fn().mockReturnValue({
requestCode,
}) as jest.Mock<typeof window.google.accounts.oauth2.initCodeClient>;
window.google = {
accounts: {
// @ts-ignore
oauth2: {
initCodeClient: mockInitCodeClient,
} as typeof window.google.accounts.oauth2,
} as typeof window.google.accounts,
};
mockRouterPush = jest.fn();
jest.spyOn(navigation, 'useRouter').mockImplementation(() => ({
push: mockRouterPush as AppRouterInstance['push'],
} as AppRouterInstance));

mockSetCredentials = jest.fn();
jest.spyOn(sessionHook, 'default').mockReturnValue({
session: undefined,
setCredentials: mockSetCredentials as React.Dispatch<
React.SetStateAction<Credentials | undefined> >,
} as sessionHook.SessionReturn);
jest.spyOn(helpers_env, 'isStaging').mockReturnValue(false);
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: false,
} as auth0.Auth0ContextInterface<auth0.User>);
});

it('shows loading... when not finished', () => {
const { container } = render(<LoginPage />);
expect(container).toMatchSnapshot();
});

it('calls requestCode when clicking sign in button', async () => {
it('sends to dashboard when authenticated', async () => {
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: true,
} as auth0.Auth0ContextInterface<auth0.User>);
render(<LoginPage />);

expect(mockInitCodeClient).toHaveBeenCalledWith({
callback: expect.any(Function),
client_id: '123339406534-gnk10bh5hqo87qlla8e9gmol1j961rtg.apps.googleusercontent.com',
scope: 'https://www.googleapis.com/auth/drive.file',
ux_mode: 'popup',
});

expect(requestCode).toBeCalledTimes(0);
screen.getByText('Sign In').click();
expect(requestCode).toBeCalledTimes(1);
expect(mockRouterPush).toHaveBeenCalledWith('/dashboard/accounts');
});

it('callback authorizes, saves session to storage and navigates to accounts page', async () => {
const credentials = {
access_token: 'access_token',
id_token: 'id_token',
refresh_token: 'refresh_token',
expiry_date: 123,
scope: '',
token_type: '',
};
jest.spyOn(Stocker, 'authorize').mockResolvedValue(credentials);

it('sends to dashboard when staging', async () => {
jest.spyOn(helpers_env, 'isStaging').mockReturnValue(true);
render(<LoginPage />);

const { callback } = mockInitCodeClient.mock.calls[0][0];
await act(async () => callback({ code: 'CODE' }));

expect(Stocker.authorize).toBeCalledWith('CODE');
expect(mockSetCredentials).toHaveBeenNthCalledWith(
1,
credentials,
);
expect(mockRouterPush).toHaveBeenCalledWith('/dashboard/accounts');
});

it('does not call requestCode when clicking sign in button and sends to /home/dashboard when staging', async () => {
jest.spyOn(helpers_env, 'isStaging').mockReturnValue(true);
render(<LoginPage />);
it('shows loading... when not finished', () => {
const { container } = render(<LoginPage />);
expect(container).toMatchSnapshot();
});

expect(mockInitCodeClient).toHaveBeenCalledWith({
callback: expect.any(Function),
client_id: '123339406534-gnk10bh5hqo87qlla8e9gmol1j961rtg.apps.googleusercontent.com',
scope: 'https://www.googleapis.com/auth/drive.file',
ux_mode: 'popup',
});
it('calls loginWithPopup when clicking sign in button', async () => {
const mockLogin = jest.fn();
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: true,
loginWithPopup: mockLogin as Function,
} as auth0.Auth0ContextInterface<auth0.User>);

render(<LoginPage />);
screen.getByText('Sign In').click();
expect(requestCode).toBeCalledTimes(0);
expect(mockRouterPush).toHaveBeenCalledWith('/dashboard/accounts');
process.env.NEXT_PUBLIC_ENV = '';

expect(mockLogin).toBeCalled();
});
});
24 changes: 14 additions & 10 deletions src/__tests__/app/user/logout/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import type { LinkProps } from 'next/link';
import * as auth0 from '@auth0/auth0-react';

import LogoutPage from '@/app/user/logout/page';
import * as sessionHook from '@/hooks/useSession';

jest.mock('next/link', () => jest.fn(
(
Expand All @@ -13,29 +13,33 @@ jest.mock('next/link', () => jest.fn(
),
));

jest.mock('@/hooks/useSession', () => ({
jest.mock('@auth0/auth0-react', () => ({
__esModule: true,
...jest.requireActual('@/hooks/useSession'),
...jest.requireActual('@auth0/auth0-react'),
}));

describe('LogoutPage', () => {
let mockRevoke: jest.Mock;
let mockLogout: jest.Mock;

beforeEach(() => {
mockRevoke = jest.fn();
jest.spyOn(sessionHook, 'default').mockReturnValue({
revoke: mockRevoke as Function,
} as sessionHook.SessionReturn);
mockLogout = jest.fn();
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
logout: mockLogout as Function,
} as auth0.Auth0ContextInterface<auth0.User>);
});

it('matches snapshot', () => {
const { container } = render(<LogoutPage />);
expect(container).toMatchSnapshot();
});

it('sets empty accessToken', () => {
it('calls logout', () => {
render(<LogoutPage />);

expect(mockRevoke).toBeCalled();
expect(mockLogout).toBeCalledWith({
logoutParams: {
returnTo: 'http://localhost/user/login',
},
});
});
});
6 changes: 3 additions & 3 deletions src/__tests__/components/ProfileDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render } from '@testing-library/react';

import ProfileDropdown from '@/components/ProfileDropdown';
import * as sessionHook from '@/hooks/useSession';
import { User } from '@/types/user';
import type { User } from '@auth0/auth0-react';

jest.mock('@/hooks/useSession', () => ({
__esModule: true,
Expand All @@ -19,7 +19,7 @@ describe('ProfileDropdown', () => {
jest.spyOn(sessionHook, 'default').mockReturnValue({
user: {
name: '',
image: '',
picture: '',
email: '',
} as User,
} as sessionHook.SessionReturn);
Expand All @@ -34,7 +34,7 @@ describe('ProfileDropdown', () => {
jest.spyOn(sessionHook, 'default').mockReturnValue({
user: {
name: 'Maffin IO',
image: 'https://example.com',
picture: 'https://example.com',
email: '[email protected]',
} as User,
} as sessionHook.SessionReturn);
Expand Down
7 changes: 2 additions & 5 deletions src/__tests__/hooks/useGapiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { act, renderHook } from '@testing-library/react';
import useGapiClient from '@/hooks/useGapiClient';
import * as sessionHook from '@/hooks/useSession';
import * as helpers_env from '@/helpers/env';
import type { Credentials } from '@/types/user';

jest.mock('@/hooks/useSession', () => ({
__esModule: true,
Expand All @@ -22,8 +21,7 @@ describe('useGapiClient', () => {

jest.spyOn(helpers_env, 'isStaging').mockReturnValue(false);
jest.spyOn(sessionHook, 'default').mockReturnValue({
session: undefined,
setCredentials: jest.fn() as Function,
accessToken: '',
} as sessionHook.SessionReturn);
});

Expand Down Expand Up @@ -118,8 +116,7 @@ describe('useGapiClient', () => {
it('returns true when script onload finished and session and loads needed libraries', async () => {
jest.spyOn(sessionHook, 'default').mockReturnValue(
{
session: { access_token: 'access_token' } as Credentials,
setCredentials: jest.fn() as Function,
accessToken: 'access_token',
} as sessionHook.SessionReturn,
);
const mockGapiClientLoad: jest.MockedFunction<typeof window.gapi.client.load> = jest.fn();
Expand Down
Loading
Loading