主文件:
import { useContext, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import AuthenticationContext from './AuthenticationContext';
function OAuthCallbackRoute() {
const { fetchToken, callbackPath, setError } = useContext(AuthenticationContext);
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (location.pathname !== callbackPath) return;
async function completeLogin(code: string, state: string) {
if (await fetchToken(code)) navigate(decodeURIComponent(state));
}
const successRedirectPathRegex = /\?code=(\S+)&state=(.+)/;
const errorRedirectPathRegex = /\?error=(\S+)&error_description=(.+)&state=(.+)/;
const successPathMatch = location.search.match(successRedirectPathRegex);
if (successPathMatch) {
const [, code, state] = successPathMatch;
completeLogin(code, decodeURIComponent(state));
}
const errorRedirectPathMatch = location.search.match(errorRedirectPathRegex);
if (errorRedirectPathMatch) {
const [, error, errorDescription] = errorRedirectPathMatch;
setError(${error} - ${decodeURIComponent(errorDescription)});
}
}, [fetchToken, navigate, location, callbackPath, setError]);
return null;
}
export default OAuthCallbackRoute;
测试文件:
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { OAuthCallbackRoute } from '..';
import AuthenticationContext from '../AuthenticationContext';
const delay = async (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
describe('OAuthCallbackRoute', () => {
let history: any;
let historyReplace: any;
let fetchToken: any;
let setError: any;
const originalError = global.console.error;
beforeEach(() => {
history = createMemoryHistory();
global.console.error = jest.fn();
jest.clearAllMocks();
});
afterEach(() => {
global.console.error = originalError;
});
function App() {
return (
<AuthenticationContext.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{
fetchToken,
callbackPath: '/oauth',
setError,
authenticated: true,
authenticating: false,
authorize: () => {},
logout: () => {},
getToken: () => 'xyz',
}}
>
<Router history={history}>
<OAuthCallbackRoute />
</Router>
</AuthenticationContext.Provider>
);
}
describe('for an OAuth callback', () => {
describe('on successful OAuth callback', () => {
beforeEach(() => {
history.replace('/oauth?code=xyz&state=%2Ffoo');
historyReplace = jest.spyOn(history, 'replace');
});
describe('on successful token fetching', () => {
beforeEach(() => {
fetchToken = jest.fn(() => true);
});
it('uses the received code to fetch an auth token', () => {
render(<App />);
expect(fetchToken).toHaveBeenCalledWith('xyz');
});
it('redirects to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).toHaveBeenCalledWith('/foo');
});
});
describe('on failed token fetching', () => {
beforeEach(() => {
fetchToken = jest.fn(() => false);
});
it('does not redirect to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
});
});
describe('on error included in callback', () => {
beforeEach(() => {
setError = jest.fn(() => true);
history.replace(
'/oauth?error=access_denied&error_description=Service%20not%20found&state=%2Ffoo',
);
historyReplace = jest.spyOn(history, 'replace');
});
it('does not redirect to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
it('puts the error on the context', async () => {
render(<App />);
expect(setError).toHaveBeenCalledWith('access_denied - Service not found');
});
});
});
describe('for a non-OAuth path', () => {
beforeEach(() => {
history.replace('/random-path');
historyReplace = jest.spyOn(history, 'replace');
});
it('does not attempt to fetch a token', async () => {
render(<App />);
await delay(100);
expect(fetchToken).not.toHaveBeenCalled();
});
it('does not redirect', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
});
});
错误:
packages/auth/src/tests/OAuthCallbackRoute.test.tsx:46:17 - 错误 TS2322:键入 '{children:Element;历史:任何; }' 不可分配给类型“IntrinsicAttributes & RouterProps”。 类型“IntrinsicAttributes & RouterProps”上不存在属性“history”。
46 <Router history={history}>
我尝试使用导航器更改历史记录,但出现了不同的错误。
欣赏一些关于“历史”错误的解决方案。
低级
Router
组件需要一些道具,其中两个是必需的,但都不是 history
。
declare function Router( props: RouterProps ): React.ReactElement | null; interface RouterProps { basename?: string; children?: React.ReactNode; location: Partial<Location> | string; // <-- Required navigationType?: NavigationType; navigator: Navigator; // <-- Required static?: boolean; }
也就是说,几乎没有理由使用
Router
组件。通常,您将在单元测试中导入并使用 MemoryRouter
。在这种情况下,您似乎有需要引用自定义历史对象的测试。为此,您可以导入并使用 HistoryRouter
,其中 确实 需要 history
道具。
export interface HistoryRouterProps {
basename?: string;
children?: React.ReactNode;
future?: FutureConfig;
history: History; // <-- custom history object
}
示例:
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { unstable_HistoryRouter as Router } from 'react-router-dom';
import { OAuthCallbackRoute } from '..';
import AuthenticationContext from '../AuthenticationContext';
const delay = async (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
describe('OAuthCallbackRoute', () => {
let history: any;
let historyReplace: any;
let fetchToken: any;
let setError: any;
const originalError = global.console.error;
beforeEach(() => {
history = createMemoryHistory();
global.console.error = jest.fn();
jest.clearAllMocks();
});
afterEach(() => {
global.console.error = originalError;
});
function App() {
return (
<AuthenticationContext.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{
fetchToken,
callbackPath: '/oauth',
setError,
authenticated: true,
authenticating: false,
authorize: () => {},
logout: () => {},
getToken: () => 'xyz',
}}
>
<Router history={history}>
<OAuthCallbackRoute />
</Router>
</AuthenticationContext.Provider>
);
}
describe('for an OAuth callback', () => {
describe('on successful OAuth callback', () => {
beforeEach(() => {
history.replace('/oauth?code=xyz&state=%2Ffoo');
historyReplace = jest.spyOn(history, 'replace');
});
describe('on successful token fetching', () => {
beforeEach(() => {
fetchToken = jest.fn(() => true);
});
it('uses the received code to fetch an auth token', () => {
render(<App />);
expect(fetchToken).toHaveBeenCalledWith('xyz');
});
it('redirects to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).toHaveBeenCalledWith('/foo');
});
});
describe('on failed token fetching', () => {
beforeEach(() => {
fetchToken = jest.fn(() => false);
});
it('does not redirect to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
});
});
describe('on error included in callback', () => {
beforeEach(() => {
setError = jest.fn(() => true);
history.replace(
'/oauth?error=access_denied&error_description=Service%20not%20found&state=%2Ffoo',
);
historyReplace = jest.spyOn(history, 'replace');
});
it('does not redirect to the referrer path', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
it('puts the error on the context', async () => {
render(<App />);
expect(setError).toHaveBeenCalledWith('access_denied - Service not found');
});
});
});
describe('for a non-OAuth path', () => {
beforeEach(() => {
history.replace('/random-path');
historyReplace = jest.spyOn(history, 'replace');
});
it('does not attempt to fetch a token', async () => {
render(<App />);
await delay(100);
expect(fetchToken).not.toHaveBeenCalled();
});
it('does not redirect', async () => {
render(<App />);
await delay(100);
expect(historyReplace).not.toHaveBeenCalled();
});
});
});