refactor: apply second round of suggestions
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
+20
-9
@@ -18,10 +18,19 @@ import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
MockStorageApi,
|
||||
TestApiProvider,
|
||||
renderInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
describe('CookieAuthRefreshProvider', () => {
|
||||
const storageApiMock = MockStorageApi.create();
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
@@ -33,12 +42,6 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
return new Date(Date.now() + tenMinutesInMilliseconds).toISOString();
|
||||
}
|
||||
|
||||
global.BroadcastChannel = jest.fn().mockImplementation(() => ({
|
||||
postMessage: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
it('should render a progress bar', async () => {
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockReturnValue(new Promise(() => {})),
|
||||
@@ -48,6 +51,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -57,7 +61,9 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('progress')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('progress')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render an error panel', async () => {
|
||||
@@ -70,6 +76,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -100,6 +107,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -118,6 +126,8 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(error.message)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Retry'));
|
||||
@@ -143,6 +153,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
|
||||
+22
-15
@@ -18,13 +18,20 @@ import React, { ReactNode } from 'react';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useCookieAuthRefresh, CookieAuthRefreshOptions } from '../../hooks';
|
||||
import { useCookieAuthRefresh } from '../../hooks';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Props for the {@link CookieAuthRefreshProvider} component.
|
||||
*/
|
||||
export type CookieAuthRefreshProviderProps = CookieAuthRefreshOptions & {
|
||||
export type CookieAuthRefreshProviderProps = {
|
||||
// The plugin ID to used for discovering the API origin
|
||||
pluginId: string;
|
||||
// Options for configuring the refresh cookie endpoint
|
||||
options?: {
|
||||
// The path to used for calling the refresh cookie endpoint, default to '/cookie'
|
||||
path?: string;
|
||||
};
|
||||
// The children to render when the refresh is successful
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -33,28 +40,28 @@ export type CookieAuthRefreshProviderProps = CookieAuthRefreshOptions & {
|
||||
* @public
|
||||
* A provider that will refresh the cookie when it is about to expire.
|
||||
*/
|
||||
export function CookieAuthRefreshProvider({
|
||||
children,
|
||||
...rest
|
||||
}: CookieAuthRefreshProviderProps) {
|
||||
export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, ...params } = props;
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const { state, actions } = useCookieAuthRefresh(rest);
|
||||
const { loading, error, retry } = useCookieAuthRefresh(params);
|
||||
|
||||
if (state.status === 'error' && state.error) {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPanel error={state.error}>
|
||||
<Button variant="outlined" onClick={actions.execute}>
|
||||
<ErrorPanel error={error}>
|
||||
<Button variant="outlined" onClick={retry}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === 'loading') {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return children;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
useCookieAuthRefresh,
|
||||
type CookieAuthRefreshOptions,
|
||||
} from './useCookieAuthRefresh';
|
||||
export { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
fetchApiRef,
|
||||
discoveryApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { MockStorageApi, TestApiProvider } from '@backstage/test-utils';
|
||||
import { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
|
||||
describe('useCookieAuthRefresh', () => {
|
||||
@@ -39,31 +43,11 @@ describe('useCookieAuthRefresh', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
type Listener = (event: { data: any }) => void;
|
||||
|
||||
let listeners: Listener[];
|
||||
let channelMock: any;
|
||||
const storageApiMock = MockStorageApi.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now });
|
||||
jest.clearAllMocks();
|
||||
listeners = [];
|
||||
channelMock = {
|
||||
postMessage: jest.fn((message: any) => {
|
||||
listeners.forEach(listener => listener({ data: message }));
|
||||
}),
|
||||
addEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}),
|
||||
removeEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners = listeners.filter(l => l !== listener);
|
||||
}
|
||||
}),
|
||||
};
|
||||
global.BroadcastChannel = jest.fn().mockImplementation(() => channelMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -83,6 +67,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
fetch: jest.fn(),
|
||||
},
|
||||
],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -92,7 +77,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.state.status).toBe('loading');
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return an error status when the refresh has failed', async () => {
|
||||
@@ -110,6 +95,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
},
|
||||
],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -119,9 +105,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.state.status).toBe('error'));
|
||||
|
||||
expect(result.current.state.error).toStrictEqual(error);
|
||||
await waitFor(() => expect(result.current.error).toStrictEqual(error));
|
||||
});
|
||||
|
||||
it('should call the api to get the cookie and use it', async () => {
|
||||
@@ -132,6 +116,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -148,46 +133,18 @@ describe('useCookieAuthRefresh', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt });
|
||||
});
|
||||
|
||||
it('should send a message to other tabs when the cookie is refreshed', async () => {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(global.BroadcastChannel).toHaveBeenCalledWith(
|
||||
'techdocs-auth-cookie-channel',
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(channelMock.postMessage).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
// posting the message to other tabs when the cookie is requested in the first time
|
||||
await waitFor(() =>
|
||||
expect(channelMock.postMessage).toHaveBeenCalledWith({
|
||||
action: 'COOKIE_REFRESHED',
|
||||
payload: { expiresAt },
|
||||
}),
|
||||
);
|
||||
expect(result.current.value).toMatchObject({ expiresAt });
|
||||
});
|
||||
|
||||
it('should cancel the refresh when a message is received from another tab', async () => {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
const pluginId = 'techdocs';
|
||||
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -196,29 +153,25 @@ describe('useCookieAuthRefresh', () => {
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(channelMock.addEventListener).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
const twentyMinutesFromNowInMilliseconds =
|
||||
now + 2 * tenMinutesInMilliseconds;
|
||||
|
||||
// simulating other tab refreshing the cookie
|
||||
channelMock.postMessage({
|
||||
action: 'COOKIE_REFRESHED',
|
||||
payload: {
|
||||
expiresAt: new Date(twentyMinutesFromNowInMilliseconds).toISOString(),
|
||||
},
|
||||
});
|
||||
storageApiMock
|
||||
.forBucket(`${pluginId}-auth-cookie-storage`)
|
||||
.set(
|
||||
'expiresAt',
|
||||
new Date(twentyMinutesFromNowInMilliseconds).toISOString(),
|
||||
);
|
||||
|
||||
// advance the timers in 10 minutes to match the old expires at
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should not call the api
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
// advance the timers in more 10 minutes to match the new expires at
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds - 1000);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
@@ -232,6 +185,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -243,16 +197,10 @@ describe('useCookieAuthRefresh', () => {
|
||||
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt });
|
||||
expect(result.current.value).toMatchObject({ expiresAt });
|
||||
|
||||
unmount();
|
||||
|
||||
expect(channelMock.removeEventListener).toHaveBeenCalledTimes(1);
|
||||
expect(channelMock.removeEventListener).toHaveBeenCalledWith(
|
||||
'message',
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// advance the timers to ensure that the refresh is not called
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
@@ -268,6 +216,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[storageApiRef, storageApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
@@ -279,7 +228,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
// advance the timers to the expiration date
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds - 1000);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
|
||||
@@ -14,51 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
storageApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
type CookieAuthRefreshMessage = MessageEvent<{
|
||||
action: string;
|
||||
payload: {
|
||||
expiresAt: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* The options for the {@link useCookieAuthRefresh} hook.
|
||||
*/
|
||||
export type CookieAuthRefreshOptions = {
|
||||
// The plugin ID to used for discovering the API origin
|
||||
pluginId: string;
|
||||
// The path to used for calling the refresh cookie endpoint, default to '/cookie'
|
||||
path?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A hook that will refresh the cookie when it is about to expire.
|
||||
* @remarks
|
||||
* This hook expects a `BroadcastChannel` to be available in the global scope.
|
||||
*/
|
||||
export function useCookieAuthRefresh({
|
||||
pluginId,
|
||||
path = '/cookie',
|
||||
}: CookieAuthRefreshOptions) {
|
||||
export function useCookieAuthRefresh(params: {
|
||||
// The plugin ID to used for discovering the API origin
|
||||
pluginId: string;
|
||||
// Options for configuring the refresh cookie endpoint
|
||||
options?: {
|
||||
// The path to used for calling the refresh cookie endpoint, default to '/cookie'
|
||||
path?: string;
|
||||
};
|
||||
}) {
|
||||
const { pluginId, options: { path = '/cookie' } = {} } = params;
|
||||
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const storageApi = useApi(storageApiRef);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
|
||||
const [channel] = useState(
|
||||
() => new BroadcastChannel(`${pluginId}-auth-cookie-channel`),
|
||||
);
|
||||
const store = storageApi.forBucket(`${pluginId}-auth-cookie-storage`);
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const [state, actions] = useAsync<{ expiresAt: string }>(async () => {
|
||||
const apiOrigin = await discoveryApi.getBaseUrl(pluginId);
|
||||
const requestUrl = `${apiOrigin}${path}`;
|
||||
const response = await fetchApi.fetch(`${requestUrl}`, {
|
||||
@@ -86,28 +73,27 @@ export function useCookieAuthRefresh({
|
||||
useEffect(() => {
|
||||
if (!state.result) return () => {};
|
||||
|
||||
channel.postMessage({
|
||||
action: 'COOKIE_REFRESHED',
|
||||
payload: state.result,
|
||||
});
|
||||
store.set('expiresAt', state.result.expiresAt);
|
||||
|
||||
let cancel = refresh(state.result);
|
||||
|
||||
const handleMessage = (event: CookieAuthRefreshMessage): void => {
|
||||
const { action, payload } = event.data;
|
||||
if (action === 'COOKIE_REFRESHED') {
|
||||
cancel();
|
||||
cancel = refresh(payload);
|
||||
}
|
||||
};
|
||||
|
||||
channel.addEventListener('message', handleMessage);
|
||||
const observable = store.observe$<string>('expiresAt');
|
||||
const subscription = observable.subscribe(({ value }) => {
|
||||
if (!value) return;
|
||||
cancel();
|
||||
cancel = refresh({ expiresAt: value });
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancel();
|
||||
channel.removeEventListener('message', handleMessage);
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [state, refresh, channel]);
|
||||
}, [state, refresh, store]);
|
||||
|
||||
return { state, actions };
|
||||
return {
|
||||
loading: state.status === 'loading',
|
||||
error: state.error,
|
||||
value: state.result,
|
||||
retry: actions.execute,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,28 +14,3 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
type Listener = (event: { data: any }) => void;
|
||||
|
||||
global.BroadcastChannel = jest
|
||||
.fn()
|
||||
.mockImplementation((_channelName: string) => {
|
||||
let listeners: Listener[] = [];
|
||||
return {
|
||||
postMessage: jest.fn((message: any) => {
|
||||
// Simulate message event for all listeners
|
||||
listeners.forEach(listener => listener({ data: message }));
|
||||
}),
|
||||
addEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}),
|
||||
removeEventListener: jest.fn((event: string, listener: Listener) => {
|
||||
if (event === 'message') {
|
||||
listeners = listeners.filter(l => l !== listener);
|
||||
}
|
||||
}),
|
||||
close: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user