Skip to content

Commit

Permalink
Use roles for premium/standard ui (#920)
Browse files Browse the repository at this point in the history
  • Loading branch information
argaen authored May 26, 2024
1 parent d50c11a commit 89008d0
Show file tree
Hide file tree
Showing 21 changed files with 138 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
NEXT_PUBLIC_AUTH0_DOMAIN=maffin-dev.eu.auth0.com
NEXT_PUBLIC_AUTH0_CLIENT_ID=mMmnR4NbQOnim9B8QZfe9wfFuaKb8rwW
NEXT_PUBLIC_AUTH0_SCOPES=profile email
NEXT_PUBLIC_AUTH0_SCOPES=profile email https://www.googleapis.com/auth/drive.file
2 changes: 1 addition & 1 deletion .env.staging
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
NEXT_PUBLIC_AUTH0_DOMAIN=maffin-dev.eu.auth0.com
NEXT_PUBLIC_AUTH0_CLIENT_ID=mMmnR4NbQOnim9B8QZfe9wfFuaKb8rwW
NEXT_PUBLIC_AUTH0_SCOPES=profile email
NEXT_PUBLIC_AUTH0_SCOPES=profile email https://www.googleapis.com/auth/drive.file
24 changes: 18 additions & 6 deletions src/__tests__/app/actions/getTodayPrices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,24 @@ jest.mock('@/lib/jwt', () => ({
}));

describe('getTodayPrices', () => {
beforeEach(() => {
jest.spyOn(jwt, 'verify').mockImplementation();
});

afterEach(() => {
jest.clearAllMocks();
});

it('throws error when token not verified', async () => {
jest.spyOn(jwt, 'verify').mockImplementation(() => { throw new Error('fail'); });

await expect(() => getTodayPrices({
tickers: ['A'],
})).rejects.toThrow('fail');
});

it('returns empty when not premium user', async () => {
jest.spyOn(jwt, 'verify').mockResolvedValue({
'https://maffin/roles': [],
});
jest.spyOn(jwt, 'isPremium').mockResolvedValue(false);
jest.spyOn(yahoo, 'default').mockImplementation();

const prices = await getTodayPrices({
Expand All @@ -28,9 +42,7 @@ describe('getTodayPrices', () => {
});

it('calls getPrices when premium user', async () => {
jest.spyOn(jwt, 'verify').mockResolvedValue({
'https://maffin/roles': ['premium'],
});
jest.spyOn(jwt, 'isPremium').mockResolvedValue(true);

const yahooPrices = {
A: {
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/components/CommodityCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ import * as apiHooks from '@/hooks/api';
import { PriceDBMap } from '@/book/prices';
import type { Commodity, Price } from '@/book/entities';
import CommodityCard from '@/components/CommodityCard';
import * as sessionHook from '@/hooks/useSession';

jest.mock('@/hooks/api');

jest.mock('@/components/Loading', () => jest.fn(
() => <div data-testid="Loading" />,
));

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

describe('CommodityCard', () => {
let eur: Commodity;
beforeEach(() => {
jest.spyOn(sessionHook, 'default').mockReturnValue({ isPremium: true } as sessionHook.SessionReturn);
jest.spyOn(apiHooks, 'useCommodity').mockReturnValue({ data: undefined } as UseQueryResult<Commodity>);
jest.spyOn(apiHooks, 'usePrices').mockReturnValue({ data: undefined } as UseQueryResult<PriceDBMap>);

Expand Down
11 changes: 5 additions & 6 deletions src/__tests__/components/buttons/SaveButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ import * as query from '@tanstack/react-query';
import SaveButton from '@/components/buttons/SaveButton';
import { DataSourceContext } from '@/hooks';
import type { DataSourceContextType } from '@/hooks';
import * as helpers_env from '@/helpers/env';
import * as stateHooks from '@/hooks/state';
import * as sessionHook from '@/hooks/useSession';

jest.mock('@tanstack/react-query');
jest.mock('@/lib/prices');

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

jest.mock('@/hooks/state', () => ({
Expand All @@ -32,6 +30,7 @@ describe('SaveButton', () => {
beforeEach(() => {
jest.spyOn(query, 'useQuery').mockReturnValue({ data: false } as query.UseQueryResult<boolean>);
jest.spyOn(stateHooks, 'useOnline').mockReturnValue({ isOnline: true });
jest.spyOn(sessionHook, 'default').mockReturnValue({ isPremium: true } as sessionHook.SessionReturn);
});

it('loads while unavailable datasource', async () => {
Expand Down Expand Up @@ -103,7 +102,7 @@ describe('SaveButton', () => {
});

it('is disabled when not PAID_PLAN', async () => {
jest.spyOn(helpers_env, 'IS_PAID_PLAN', 'get').mockReturnValue(false);
jest.spyOn(sessionHook, 'default').mockReturnValue({ isPremium: false } as sessionHook.SessionReturn);
render(
<DataSourceContext.Provider value={{ isLoaded: true } as DataSourceContextType}>
<SaveButton />
Expand Down
18 changes: 6 additions & 12 deletions src/__tests__/hooks/useBookStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useBookStorage from '@/hooks/useBookStorage';
import * as gapiHooks from '@/hooks/useGapiClient';
import BookStorage from '@/lib/storage/GDriveBookStorage';
import DemoBookStorage from '@/lib/storage/DemoBookStorage';
import * as helpers_env from '@/helpers/env';
import * as sessionHook from '@/hooks/useSession';

jest.mock('@/hooks/useGapiClient', () => ({
__esModule: true,
Expand All @@ -13,20 +13,15 @@ jest.mock('@/hooks/useGapiClient', () => ({

jest.mock('@/lib/storage/GDriveBookStorage');

jest.mock('@/helpers/env', () => ({
jest.mock('@/hooks/useSession', () => ({
__esModule: true,
get IS_DEMO_PLAN() {
return false;
},
get IS_PAID_PLAN() {
return false;
},
...jest.requireActual('@/hooks/useSession'),
}));

describe('useBookStorage', () => {
beforeEach(() => {
jest.spyOn(gapiHooks, 'default').mockReturnValue([false]);
jest.spyOn(helpers_env, 'IS_PAID_PLAN', 'get').mockReturnValue(true);
jest.spyOn(sessionHook, 'default').mockReturnValue({ isPremium: true } as sessionHook.SessionReturn);
});

afterEach(() => {
Expand All @@ -53,9 +48,8 @@ describe('useBookStorage', () => {
});
});

it('returns DemoStorage when IS_DEMO_PLAN', async () => {
jest.spyOn(helpers_env, 'IS_PAID_PLAN', 'get').mockReturnValue(false);
jest.spyOn(helpers_env, 'IS_DEMO_PLAN', 'get').mockReturnValue(true);
it('returns DemoStorage when not premium', async () => {
jest.spyOn(sessionHook, 'default').mockReturnValue({ isPremium: false } as sessionHook.SessionReturn);
window.gapi = {
client: {} as typeof gapi.client,
} as typeof gapi;
Expand Down
16 changes: 0 additions & 16 deletions src/__tests__/hooks/useGapiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,17 @@ import { act, renderHook, waitFor } from '@testing-library/react';

import useGapiClient from '@/hooks/useGapiClient';
import * as sessionHook from '@/hooks/useSession';
import * as helpers_env from '@/helpers/env';

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

jest.mock('@/helpers/env', () => ({
__esModule: true,
get IS_PAID_PLAN() {
return true;
},
}));

describe('useGapiClient', () => {
beforeEach(() => {
// @ts-ignore
window.gapi = null;

jest.spyOn(helpers_env, 'IS_PAID_PLAN', 'get').mockReturnValue(true);
jest.spyOn(sessionHook, 'default').mockReturnValue({
accessToken: '',
} as sessionHook.SessionReturn);
Expand Down Expand Up @@ -65,13 +56,6 @@ describe('useGapiClient', () => {
expect(document.body.getElementsByTagName('script').length).toEqual(1);
});

it('returns true when not PAID_PLAN', () => {
jest.spyOn(helpers_env, 'IS_PAID_PLAN', 'get').mockReturnValue(false);
const { result } = renderHook(() => useGapiClient());

expect(result.current).toEqual([true]);
});

it('returns false when script onload not finished', () => {
const { result } = renderHook(() => useGapiClient());

Expand Down
22 changes: 20 additions & 2 deletions src/__tests__/hooks/useSession.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import * as auth0 from '@auth0/auth0-react';

import useSession from '@/hooks/useSession';
import * as jwt from '@/lib/jwt';

jest.mock('next/navigation');

Expand All @@ -10,20 +11,24 @@ jest.mock('@auth0/auth0-react', () => ({
...jest.requireActual('@auth0/auth0-react'),
}));

jest.mock('@/helpers/env', () => ({
jest.mock('@/lib/jwt', () => ({
__esModule: true,
...jest.requireActual('@/lib/jwt'),
}));

describe('useSession', () => {
beforeEach(() => {
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: false,
getAccessTokenSilently: jest.fn() as Function,
} as auth0.Auth0ContextInterface<auth0.User>);
jest.spyOn(jwt, 'isPremium').mockResolvedValue(false);
});

it('returns emptyUser when no user', async () => {
const { result } = renderHook(() => useSession());

expect(result.current.isPremium).toBe(false);
expect(result.current.accessToken).toEqual('');
expect(result.current.user).toEqual({
email: '',
Expand All @@ -41,11 +46,24 @@ describe('useSession', () => {
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: true,
user,
getAccessTokenSilently: jest.fn() as Function,
} as auth0.Auth0ContextInterface<auth0.User>);

const { result } = renderHook(() => useSession());

expect(result.current.accessToken).toEqual('accessToken');
expect(result.current.user).toEqual(user);
});

it('returns isPremium to true when premium', async () => {
jest.spyOn(jwt, 'isPremium').mockResolvedValue(true);
jest.spyOn(auth0, 'useAuth0').mockReturnValue({
isAuthenticated: true,
getAccessTokenSilently: jest.fn() as Function,
} as auth0.Auth0ContextInterface<auth0.User>);

const { result } = renderHook(() => useSession());

await waitFor(() => expect(result.current.isPremium).toBe(true));
});
});
20 changes: 19 additions & 1 deletion src/__tests__/lib/jwt.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import jwt from 'jsonwebtoken';

import { verify } from '@/lib/jwt';
import { isPremium, verify } from '@/lib/jwt';

describe('verify', () => {
beforeEach(() => {
Expand Down Expand Up @@ -28,3 +28,21 @@ describe('verify', () => {
await expect(verify('token')).rejects.toThrow('fail');
});
});

describe('isPremium', () => {
it('returns true when premium', async () => {
jest.spyOn(jwt, 'decode').mockReturnValue({
'https://maffin/roles': ['premium'],
});

expect(await isPremium('token')).toBe(true);
});

it('returns false when not premium', async () => {
jest.spyOn(jwt, 'decode').mockReturnValue({
'https://maffin/roles': [],
});

expect(await isPremium('token')).toBe(false);
});
});
11 changes: 9 additions & 2 deletions src/__tests__/lib/storage/DemoBookStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ describe('DemoBookStorage', () => {
arrayBuffer: () => Promise.resolve(rawBook.buffer),
})) as jest.Mock,
);
Object.defineProperty(window, 'location', {
value: {
host: 'demo.maffin.io',
},
writable: true,
});

await instance.initStorage();
});

Expand All @@ -52,10 +59,10 @@ describe('DemoBookStorage', () => {
expect(content).toEqual(pako.ungzip(rawBook));
});

it('returns empty data when in free.maffin.io', async () => {
it('returns empty data when domain not demo.maffin.io', async () => {
Object.defineProperty(window, 'location', {
value: {
host: 'free.maffin.io',
host: 'app.maffin.io',
},
writable: true,
});
Expand Down
6 changes: 3 additions & 3 deletions src/app/actions/getTodayPrices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import getPrices from '@/lib/external/yahoo';
import type { Price } from '@/lib/external/yahoo';
import { verify } from '@/lib/jwt';
import { isPremium, verify } from '@/lib/jwt';

/**
* Action to retrieve prices from an external API.
Expand All @@ -17,8 +17,8 @@ export default async function getTodayPrices({
tickers: string[],
accessToken: string,
}): Promise<{ [ticker: string]: Price }> {
const token = await verify(accessToken);
if (!token['https://maffin/roles'].includes('premium')) {
await verify(accessToken);
if (!await isPremium(accessToken)) {
return {};
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/CommodityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { Tooltip, UpgradeTooltip } from '@/components/tooltips';
import { useCommodity, useMainCurrency, usePrices } from '@/hooks/api';
import Loading from '@/components/Loading';
import { IS_PAID_PLAN } from '@/helpers/env';
import useSession from '@/hooks/useSession';

export type CommodityCardProps = {
guid: string;
Expand All @@ -21,6 +21,7 @@ export default function CommodityCard({
const { data: commodity } = useCommodity(guid);
const { data: mainCurrency } = useMainCurrency();
const { data: prices } = usePrices({ from: commodity });
const { isPremium } = useSession();

if (!commodity || !prices) {
return <Loading />;
Expand Down Expand Up @@ -132,7 +133,7 @@ export default function CommodityCard({
{
!priceAvailable
&& (
IS_PAID_PLAN
isPremium
? (
<Tooltip
id={`missing-price-${guid}`}
Expand Down
Loading

0 comments on commit 89008d0

Please sign in to comment.