refactor: apply review suggestions
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
c884b9a478
commit
ffd71105a6
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { AppMode } from './AppMode';
|
||||
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
|
||||
import { componentsApiRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const now = 1710316886171;
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
const tenMinutesFromNowInMilliseconds = now + tenMinutesInMilliseconds;
|
||||
const expiresAt = new Date(tenMinutesFromNowInMilliseconds).toISOString();
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
useApp: jest.fn().mockReturnValue({
|
||||
getComponents: () => ({ Progress: () => <div data-testid="progress" /> }),
|
||||
}),
|
||||
useApi: jest.fn(ref => {
|
||||
if (ref === discoveryApiRef) {
|
||||
return {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
|
||||
};
|
||||
}
|
||||
|
||||
if (ref === fetchApiRef) {
|
||||
return {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (ref === componentsApiRef) {
|
||||
return {
|
||||
getComponent: jest
|
||||
.fn()
|
||||
.mockReturnValue(() => <div data-testid="progress" />),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Attempted to use an unmocked API reference: ${ref}`);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AppMode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers({ now });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render the children when app mode is undefined', async () => {
|
||||
render(<AppMode>Test content</AppMode>);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the children the app mode is public', async () => {
|
||||
render(
|
||||
<>
|
||||
<meta name="backstage-app-mode" content="public" />
|
||||
<AppMode>Test content</AppMode>
|
||||
</>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
|
||||
render(
|
||||
<>
|
||||
<meta name="backstage-app-mode" content="protected" />
|
||||
<AppMode>Test content</AppMode>
|
||||
</>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
|
||||
import { CompatAppProgress } from '../CompatAppProgress';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will protect the app when running in protected experimental mode.
|
||||
*/
|
||||
export function AppMode(props: { children: ReactNode }): JSX.Element {
|
||||
const { children } = props;
|
||||
|
||||
const [appMode, setAppMode] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('meta[name="backstage-app-mode"]');
|
||||
setAppMode(element?.getAttribute('content') ?? 'public');
|
||||
}, [setAppMode]);
|
||||
|
||||
if (!appMode) {
|
||||
return <CompatAppProgress />;
|
||||
}
|
||||
|
||||
if (appMode === 'protected') {
|
||||
return (
|
||||
<CookieAuthRefreshProvider pluginId="app">
|
||||
{children}
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { RedirectToRoot } from './RedirectToRoot';
|
||||
export { AppMode } from './AppMode';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
|
||||
import {
|
||||
coreComponentRefs,
|
||||
useComponentRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
function LegacyAppProgress() {
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
function NewAppProgress() {
|
||||
const Progress = useComponentRef(coreComponentRefs.progress);
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
export function CompatAppProgress() {
|
||||
const isInNewApp = !useVersionedContext<{ 1: unknown }>('app-context');
|
||||
return isInNewApp ? <NewAppProgress /> : <LegacyAppProgress />;
|
||||
}
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ExperimentalAppProtection } from './ExperimentalAppProtection';
|
||||
export { CompatAppProgress } from './CompatAppProgress';
|
||||
+1
-1
@@ -119,7 +119,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/.backstage/v1-cookie',
|
||||
'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
+2
-4
@@ -16,9 +16,9 @@
|
||||
|
||||
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 } from '../../hooks';
|
||||
import { CompatAppProgress } from '../CompatAppProgress/CompatAppProgress';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -41,13 +41,11 @@ export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, ...options } = props;
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const result = useCookieAuthRefresh(options);
|
||||
|
||||
if (result.status === 'loading') {
|
||||
return <Progress />;
|
||||
return <CompatAppProgress />;
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
|
||||
+4
-4
@@ -18,9 +18,9 @@ import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
import { identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { RedirectToRoot } from './RedirectToRoot';
|
||||
import { CookieAuthRootRedirect } from './CookieAuthRootRedirect';
|
||||
|
||||
describe('RedirectToRoot', () => {
|
||||
describe('CookieAuthRootRedirect', () => {
|
||||
const identityApiMock = { getCredentials: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -32,7 +32,7 @@ describe('RedirectToRoot', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[identityApiRef, identityApiMock]]}>
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('RedirectToRoot', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[identityApiRef, identityApiMock]]}>
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
* @public
|
||||
* A component that redirects to the root of the app after a successful sign-in.
|
||||
*/
|
||||
export function RedirectToRoot() {
|
||||
export function CookieAuthRootRedirect() {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { CookieAuthRootRedirect } from './CookieAuthRootRedirect';
|
||||
-150
@@ -1,150 +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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import {
|
||||
MockConfigApi,
|
||||
TestApiProvider,
|
||||
renderInTestApp,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/test-utils';
|
||||
import { ExperimentalAppProtection } from './ExperimentalAppProtection';
|
||||
import {
|
||||
configApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
describe('ExperimentalAppProtection', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const configApiMock = new MockConfigApi({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:7000',
|
||||
},
|
||||
});
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn(),
|
||||
};
|
||||
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the progress component while loading', async () => {
|
||||
fetchApiMock.fetch.mockReturnValueOnce(new Promise(() => {}));
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByTestId('progress')).toBeVisible();
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children even when there is an error', async () => {
|
||||
const error = new Error('Failed to fetch');
|
||||
fetchApiMock.fetch.mockRejectedValueOnce(error);
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Failed to fetch')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children even when the public index is not available', async () => {
|
||||
fetchApiMock.fetch.mockResolvedValueOnce({ ok: false });
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children also when the public index is available', async () => {
|
||||
fetchApiMock.fetch.mockResolvedValueOnce({ ok: true });
|
||||
await renderInTestApp(
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
|
||||
worker.use(
|
||||
rest.get('http://localhost:7000/public/index.html', (_, res, ctx) => {
|
||||
return res(ctx.status(200));
|
||||
}),
|
||||
rest.get(
|
||||
'http://localhost:7000/app/.backstage/v1-cookie',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({ expiresAt: Date.now() + 10 * 60 * 1000 }),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
-63
@@ -1,63 +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.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
configApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
useApp,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will protect the app when running in public experimental mode.
|
||||
*/
|
||||
export function ExperimentalAppProtection(props: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const { children } = props;
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const Components = useApp().getComponents();
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const baseUrl = configApi.getString('backend.baseUrl');
|
||||
const response = await fetchApi.fetch(`${baseUrl}/public/index.html`);
|
||||
return response.ok;
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
if (state.status === 'not-executed' || state.status === 'loading') {
|
||||
return <Components.Progress />;
|
||||
}
|
||||
|
||||
// Request failed, or the public index is not available
|
||||
if (state.status === 'error' || !state.result) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// The public index is available
|
||||
// That means the app is running in public experimental mode
|
||||
return (
|
||||
<CookieAuthRefreshProvider pluginId="app">
|
||||
{children}
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,6 @@
|
||||
// The index file in ./components/ is typically responsible for selecting
|
||||
// which components are public API and should be exported from the package.
|
||||
|
||||
export * from './RedirectToRoot';
|
||||
export * from './CookieAuthRootRedirect';
|
||||
export * from './CookieAuthRefreshProvider';
|
||||
export * from './ExperimentalAppProtection';
|
||||
export * from './AppMode';
|
||||
|
||||
@@ -236,7 +236,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/.backstage/v1-cookie',
|
||||
'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,13 +31,13 @@ import { ResponseError } from '@backstage/errors';
|
||||
export function useCookieAuthRefresh(options: {
|
||||
// The plugin id used for discovering the API origin
|
||||
pluginId: string;
|
||||
// The path used for calling the refresh cookie endpoint, default to '/.backstage/v1-cookie'
|
||||
// The path used for calling the refresh cookie endpoint, default to '/.backstage/auth/v1/cookie'
|
||||
path?: string;
|
||||
}):
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; error: Error; retry: () => void }
|
||||
| { status: 'success'; data: { expiresAt: string } } {
|
||||
const { pluginId, path = '/.backstage/v1-cookie' } = options ?? {};
|
||||
const { pluginId, path = '/.backstage/auth/v1/cookie' } = options ?? {};
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user