refactor: apply review suggestions
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-techdocs-addons-test-utils': patch
|
||||
---
|
||||
|
||||
Use the new generic refresh user cookie provider.
|
||||
|
||||
@@ -3,27 +3,22 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { AsyncState } from '@react-hookz/web';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { UseAsyncActions } from '@react-hookz/web';
|
||||
|
||||
// @public
|
||||
export type AuthApi = {
|
||||
getCookie(): Promise<{
|
||||
expiresAt: string;
|
||||
}>;
|
||||
export type CookieAuthRefreshOptions = {
|
||||
pluginId: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function CookieAuthRefreshProvider<T extends AuthApi>({
|
||||
apiRef,
|
||||
export function CookieAuthRefreshProvider({
|
||||
children,
|
||||
}: {
|
||||
apiRef: ApiRef<T>;
|
||||
children: ReactNode;
|
||||
}):
|
||||
...rest
|
||||
}: CookieAuthRefreshProviderProps):
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
@@ -33,22 +28,16 @@ export function CookieAuthRefreshProvider<T extends AuthApi>({
|
||||
| undefined;
|
||||
|
||||
// @public
|
||||
export function useCookieAuthRefresh<T extends AuthApi>({
|
||||
apiRef,
|
||||
}: {
|
||||
apiRef: ApiRef<T>;
|
||||
}): {
|
||||
state: AsyncState<
|
||||
| {
|
||||
expiresAt: string;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
actions: UseAsyncActions<
|
||||
{
|
||||
expiresAt: string;
|
||||
},
|
||||
[]
|
||||
>;
|
||||
export type CookieAuthRefreshProviderProps = CookieAuthRefreshOptions & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function useCookieAuthRefresh({
|
||||
pluginId,
|
||||
path,
|
||||
}: CookieAuthRefreshOptions): {
|
||||
state: AsyncState<any>;
|
||||
actions: UseAsyncActions<any, []>;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@react-hookz/web": "^24.0.4"
|
||||
"@react-hookz/web": "^24.0.4",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
+64
-29
@@ -19,10 +19,15 @@ 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { AuthApi } from '../../types';
|
||||
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
describe('CookieAuthRefreshProvider', () => {
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('http://localhost:7000/techdocs/api'),
|
||||
};
|
||||
|
||||
function getExpiresAtInFuture() {
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
return new Date(Date.now() + tenMinutesInMilliseconds).toISOString();
|
||||
@@ -35,14 +40,18 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
}));
|
||||
|
||||
it('should render a progress bar', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockReturnValue(new Promise(() => {})),
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockReturnValue(new Promise(() => {})),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>
|
||||
<CookieAuthRefreshProvider apiRef={apiRef}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
@@ -51,16 +60,20 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
expect(screen.getByTestId('progress')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a error panel', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
it('should render an error panel', async () => {
|
||||
const error = new Error('Failed to get cookie');
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockRejectedValue(error),
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>
|
||||
<CookieAuthRefreshProvider apiRef={apiRef}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
@@ -70,29 +83,46 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
});
|
||||
|
||||
it('should call the api again when retry is clicked', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const error = new Error('Failed to get cookie');
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockRejectedValueOnce(error).mockResolvedValue({
|
||||
expiresAt: getExpiresAtInFuture(),
|
||||
}),
|
||||
const fetchApiMock = {
|
||||
fetch: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
expiresAt: getExpiresAtInFuture(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>
|
||||
<CookieAuthRefreshProvider apiRef={apiRef}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(apiMock.getCookie).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(screen.getByText(error.message)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Retry'));
|
||||
|
||||
expect(apiMock.getCookie).toHaveBeenCalledTimes(2);
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument(),
|
||||
@@ -100,18 +130,23 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
});
|
||||
|
||||
it('should render the children', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({
|
||||
expiresAt: {
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
expiresAt: getExpiresAtInFuture(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>
|
||||
<CookieAuthRefreshProvider apiRef={apiRef}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<div>Test Content</div>
|
||||
</CookieAuthRefreshProvider>
|
||||
</TestApiProvider>,
|
||||
|
||||
+15
-11
@@ -16,27 +16,31 @@
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
import { ApiRef, useApp } from '@backstage/core-plugin-api';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useCookieAuthRefresh } from '../../hooks';
|
||||
import { AuthApi } from '../../types';
|
||||
import { useCookieAuthRefresh, CookieAuthRefreshOptions } from '../../hooks';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Props for the {@link CookieAuthRefreshProvider} component.
|
||||
*/
|
||||
export type CookieAuthRefreshProviderProps = CookieAuthRefreshOptions & {
|
||||
// The children to render when the refresh is successful
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will refresh the cookie when it is about to expire.
|
||||
* It receives an `apiRef` and `children` as props, and expects that apiRef extends the `AuthApi` interface.
|
||||
*/
|
||||
export function CookieAuthRefreshProvider<T extends AuthApi>({
|
||||
apiRef,
|
||||
export function CookieAuthRefreshProvider({
|
||||
children,
|
||||
}: {
|
||||
apiRef: ApiRef<T>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
...rest
|
||||
}: CookieAuthRefreshProviderProps) {
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const { state, actions } = useCookieAuthRefresh({ apiRef });
|
||||
const { state, actions } = useCookieAuthRefresh(rest);
|
||||
|
||||
if (state.status === 'error' && state.error) {
|
||||
return (
|
||||
|
||||
@@ -14,4 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider';
|
||||
export {
|
||||
CookieAuthRefreshProvider,
|
||||
type CookieAuthRefreshProviderProps,
|
||||
} from './CookieAuthRefreshProvider';
|
||||
|
||||
@@ -14,4 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
export {
|
||||
useCookieAuthRefresh,
|
||||
type CookieAuthRefreshOptions,
|
||||
} from './useCookieAuthRefresh';
|
||||
|
||||
@@ -16,17 +16,29 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { useCookieAuthRefresh } from './useCookieAuthRefresh';
|
||||
import { AuthApi } from '../../types';
|
||||
|
||||
describe('useCookieAuthRefresh', () => {
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('http://localhost:7000/techdocs/api'),
|
||||
};
|
||||
|
||||
const now = 1710316886171;
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
const tenMinutesFromNowInMilliseconds = now + tenMinutesInMilliseconds;
|
||||
const expiresAt = new Date(tenMinutesFromNowInMilliseconds).toISOString();
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
}),
|
||||
};
|
||||
|
||||
type Listener = (event: { data: any }) => void;
|
||||
|
||||
let listeners: Listener[];
|
||||
@@ -34,6 +46,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now });
|
||||
jest.clearAllMocks();
|
||||
listeners = [];
|
||||
channelMock = {
|
||||
postMessage: jest.fn((message: any) => {
|
||||
@@ -58,32 +71,53 @@ describe('useCookieAuthRefresh', () => {
|
||||
});
|
||||
|
||||
it('should return a loading status when the refresh is in progress', () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn(),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
),
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest.fn(),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.state.status).toBe('loading');
|
||||
});
|
||||
|
||||
it('should return an error status when the refresh has failed', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const error = new Error('Failed to get cookie');
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockRejectedValue(error),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
),
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
fetchApiRef,
|
||||
{
|
||||
fetch: jest.fn().mockRejectedValue(error),
|
||||
},
|
||||
],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.state.status).toBe('error'));
|
||||
|
||||
@@ -91,38 +125,48 @@ describe('useCookieAuthRefresh', () => {
|
||||
});
|
||||
|
||||
it('should call the api to get the cookie and use it', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(apiMock.getCookie).toHaveBeenCalled();
|
||||
const { result } = renderHook(
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt }),
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt });
|
||||
});
|
||||
|
||||
it('should send a message to other tabs when the cookie is refreshed', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
};
|
||||
|
||||
renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(global.BroadcastChannel).toHaveBeenCalledWith(
|
||||
'auth-test-auth-cookie-channel',
|
||||
'techdocs-auth-cookie-channel',
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
@@ -139,14 +183,16 @@ describe('useCookieAuthRefresh', () => {
|
||||
});
|
||||
|
||||
it('should cancel the refresh when a message is received from another tab', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
};
|
||||
|
||||
renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -169,37 +215,35 @@ describe('useCookieAuthRefresh', () => {
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should not call the api
|
||||
expect(apiMock.getCookie).toHaveBeenCalledTimes(1);
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// advance the timers in more 10 minutes to match the new expires at
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(apiMock.getCookie).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('should cancel the refresh when the component is unmounted', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
};
|
||||
|
||||
const { result, unmount } = renderHook(
|
||||
() => useCookieAuthRefresh({ apiRef }),
|
||||
() => useCookieAuthRefresh({ pluginId: 'techdocs' }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(apiMock.getCookie).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt }),
|
||||
);
|
||||
expect(result.current.state.result).toMatchObject({ expiresAt });
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -213,27 +257,31 @@ describe('useCookieAuthRefresh', () => {
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should not call the api after unmount
|
||||
await waitFor(() => expect(apiMock.getCookie).not.toHaveBeenCalledTimes(2));
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).not.toHaveBeenCalledTimes(2),
|
||||
);
|
||||
});
|
||||
|
||||
it('should refresh the cookie when it is about to expire', async () => {
|
||||
const apiRef = createApiRef<AuthApi>({ id: 'auth-test' });
|
||||
const apiMock = {
|
||||
getCookie: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
};
|
||||
|
||||
renderHook(() => useCookieAuthRefresh({ apiRef }), {
|
||||
renderHook(() => useCookieAuthRefresh({ pluginId: 'techdocs' }), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestApiProvider apis={[[apiRef, apiMock]]}>{children}</TestApiProvider>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(apiMock.getCookie).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(1));
|
||||
|
||||
// advance the timers to the expiration date
|
||||
jest.advanceTimersByTime(tenMinutesInMilliseconds);
|
||||
|
||||
// should call the api
|
||||
await waitFor(() => expect(apiMock.getCookie).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { ApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
import { AuthApi } from '../../types';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
type CookieAuthRefreshMessage = MessageEvent<{
|
||||
action: string;
|
||||
@@ -28,22 +32,43 @@ type CookieAuthRefreshMessage = MessageEvent<{
|
||||
|
||||
/**
|
||||
* @public
|
||||
* It receives an `apiRef` as options, and expects that apiRef extends the `AuthApi` interface.
|
||||
* 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<T extends AuthApi>({
|
||||
apiRef,
|
||||
}: {
|
||||
apiRef: ApiRef<T>;
|
||||
}) {
|
||||
const api = useApi(apiRef);
|
||||
export function useCookieAuthRefresh({
|
||||
pluginId,
|
||||
path = '/cookie',
|
||||
}: CookieAuthRefreshOptions) {
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
|
||||
const [channel] = useState(
|
||||
() => new BroadcastChannel(`${apiRef.id}-auth-cookie-channel`),
|
||||
() => new BroadcastChannel(`${pluginId}-auth-cookie-channel`),
|
||||
);
|
||||
|
||||
const [state, actions] = useAsync(async () => await api.getCookie());
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const apiOrigin = await discoveryApi.getBaseUrl(pluginId);
|
||||
const requestUrl = `${apiOrigin}${path}`;
|
||||
const response = await fetchApi.fetch(`${requestUrl}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
|
||||
@@ -25,4 +25,3 @@
|
||||
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Defines a minimal inteface for auth apis.
|
||||
*/
|
||||
export type AuthApi = { getCookie(): Promise<{ expiresAt: string }> };
|
||||
@@ -24,7 +24,11 @@ import { act, render } from '@testing-library/react';
|
||||
|
||||
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
TechDocsAddons,
|
||||
@@ -69,6 +73,22 @@ const scmIntegrationsApi = {
|
||||
fromConfig: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
const discoveryApi = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('https://backstage.example.com/api/techdocs'),
|
||||
};
|
||||
|
||||
const fetchApi = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
// Expires in 10 minutes
|
||||
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
/** @ignore */
|
||||
type TechDocsAddonTesterTestApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
@@ -204,6 +224,8 @@ export class TechDocsAddonTester {
|
||||
*/
|
||||
build() {
|
||||
const apis: TechdocsAddonTesterApis<any[]> = [
|
||||
[fetchApiRef, fetchApi],
|
||||
[discoveryApiRef, discoveryApi],
|
||||
[techdocsApiRef, techdocsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
|
||||
+24
-2
@@ -34,7 +34,11 @@ import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
|
||||
import { Page } from '@backstage/core-components';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
configApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
const mockEntityMetadata = {
|
||||
locationMetadata: {
|
||||
@@ -76,6 +80,22 @@ const techdocsStorageApiMock: jest.Mocked<typeof techdocsStorageApiRef.T> = {
|
||||
syncEntityDocs: jest.fn(),
|
||||
};
|
||||
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue('https://localhost:7000/api/techdocs'),
|
||||
};
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
// Expires in 10 minutes
|
||||
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const PageMock = () => {
|
||||
const { namespace, kind, name } = useParams();
|
||||
return <>{`PageMock: ${namespace}#${kind}#${name}`}</>;
|
||||
@@ -96,6 +116,8 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[fetchApiRef, fetchApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
[scmIntegrationsApiRef, {}],
|
||||
[configApiRef, configApi],
|
||||
[techdocsApiRef, techdocsApiMock],
|
||||
@@ -147,7 +169,7 @@ describe('<TechDocsReaderPage />', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
TECHDOCS_ADDONS_WRAPPER_KEY,
|
||||
TECHDOCS_ADDONS_KEY,
|
||||
TechDocsReaderPageProvider,
|
||||
techdocsApiRef,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { TechDocsReaderPageRenderFunction } from '../../../types';
|
||||
@@ -180,7 +179,7 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
|
||||
// As explained above, "page" is configuration 4 and <TechDocsReaderLayout> is 1
|
||||
return (
|
||||
<CookieAuthRefreshProvider apiRef={techdocsApiRef}>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{(page as JSX.Element) || <TechDocsReaderLayout />}
|
||||
</TechDocsReaderPageProvider>
|
||||
@@ -190,7 +189,7 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
|
||||
// As explained above, a render function is configuration 3 and React element is 2
|
||||
return (
|
||||
<CookieAuthRefreshProvider apiRef={techdocsApiRef}>
|
||||
<CookieAuthRefreshProvider pluginId="techdocs">
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{({ metadata, entityMetadata, onReady }) => (
|
||||
<div className="techdocs-reader-page">
|
||||
|
||||
@@ -5117,12 +5117,14 @@ __metadata:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@react-hookz/web": ^24.0.4
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
languageName: unknown
|
||||
|
||||
Reference in New Issue
Block a user