Skip to content

Commit

Permalink
feat(private-auth): add a form to initiate private auth flow
Browse files Browse the repository at this point in the history
  • Loading branch information
sahil143 committed Nov 22, 2023
1 parent c63d821 commit 5ccfa07
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 44 deletions.
36 changes: 18 additions & 18 deletions src/components/ImportForm/SourceSection/AuthOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { CheckIcon } from '@patternfly/react-icons/dist/js/icons/check-icon';
import { useFormikContext } from 'formik';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { useModalLauncher } from '../../modal/ModalProvider';
import { initiateSpiAuthSession, useAccessTokenBinding } from '../utils/auth-utils';
import { useAccessTokenBinding } from '../utils/auth-utils';
import { ImportFormValues } from '../utils/types';
import { createAuthTokenModal } from './AuthTokenModal';

const AuthOptions: React.FC<React.PropsWithChildren<unknown>> = () => {
const AuthOptions: React.FC = () => {
const [token, setToken] = React.useState<string>(null);
const {
values: { secret, source },
} = useFormikContext<ImportFormValues>();
Expand All @@ -28,14 +29,13 @@ const AuthOptions: React.FC<React.PropsWithChildren<unknown>> = () => {

const [{ oAuthUrl, uploadUrl }, loaded] = useAccessTokenBinding(source.git.url);

const startAuthorization = React.useCallback(async () => {
if (oAuthUrl) {
const token = await getToken();
await initiateSpiAuthSession(oAuthUrl, token);
window.open(oAuthUrl, '_blank');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [oAuthUrl]);
React.useEffect(() => {
const resolveToken = async () => {
const tkn = await getToken();
setToken(tkn);
};
resolveToken();
}, [getToken]);

return (
<FormGroup label="Authorization">
Expand All @@ -55,14 +55,14 @@ const AuthOptions: React.FC<React.PropsWithChildren<unknown>> = () => {
) : (
<>
<br />
<Button
variant={ButtonVariant.primary}
onClick={startAuthorization}
isDisabled={!loaded}
isInline
>
Sign in
</Button>
{token ? (
<form style={{ display: 'inline' }} action={oAuthUrl} target="_blank" method="POST">
<input style={{ display: 'none' }} name="k8s_token" value={token} />
<Button type="submit" variant={ButtonVariant.primary} isDisabled={!loaded} isInline>
Sign in
</Button>
</form>
) : null}
<Button
variant={ButtonVariant.link}
onClick={() => showModal(createAuthTokenModal({ uploadUrl }))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import { screen, fireEvent, act } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useFormikContext } from 'formik';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { namespaceRenderer } from '../../../../utils/test-utils';
import { useAccessTokenBinding } from '../../utils/auth-utils';
import AuthOptions from '../AuthOptions';
Expand All @@ -11,7 +12,7 @@ jest.mock('formik', () => ({
}));

jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({ auth: { getToken: () => Promise.resolve('token') } }),
useChrome: jest.fn(),
}));

jest.mock('../../utils/auth-utils', () => ({
Expand All @@ -25,12 +26,13 @@ const useFormikContextMock = useFormikContext as jest.Mock;

const useAccessTokenBindingMock = useAccessTokenBinding as jest.Mock;

const windowOpenMock = window.open as jest.Mock;
const useChromeMock = useChrome as jest.Mock;

const renderAuthOptions = () => namespaceRenderer(<AuthOptions />, 'test-ns');

describe('AuthOptions', () => {
it('should show spinner if auth url is not loaded', () => {
useChromeMock.mockReturnValue({ auth: { getToken: () => Promise.resolve('token') } });
useAccessTokenBindingMock.mockReturnValue([{}, false]);
useFormikContextMock.mockReturnValue({
values: { source: { git: { url: 'test-source' } }, secret: null },
Expand All @@ -40,6 +42,7 @@ describe('AuthOptions', () => {
});

it('should show success message if secret is available', () => {
useChromeMock.mockReturnValue({ auth: { getToken: () => Promise.resolve('token') } });
useAccessTokenBindingMock.mockReturnValue([{}, true]);
useFormikContextMock.mockReturnValue({
values: { source: { git: { url: 'test-source' } }, secret: 'test-secret' },
Expand All @@ -50,37 +53,24 @@ describe('AuthOptions', () => {
expect(screen.queryByText('Use a token instead')).not.toBeInTheDocument();
});

it('should not call window.open if auth url is not available', async () => {
it('should show render form once token is resolved', async () => {
useChromeMock.mockReturnValue({ auth: { getToken: () => Promise.resolve('token') } });
useAccessTokenBindingMock.mockReturnValue([{}, true]);
useFormikContextMock.mockReturnValue({
values: { source: { git: { url: 'https://github.com/test/repository' } }, secret: null },
values: { source: { git: { url: 'test-source' } } },
});

renderAuthOptions();
const button = screen.getByText('Sign in');
expect(button).toBeEnabled();

await act(async () => {
fireEvent.click(button);
});

expect(windowOpenMock).not.toHaveBeenCalled();
await waitFor(() => expect(screen.queryByText('Sign in')).toBeInTheDocument());
});

it('should call window.open with auth url and token', async () => {
useAccessTokenBindingMock.mockReturnValue([{ oAuthUrl: 'example.com/auth?state=abcd' }, true]);
it('should not render form if token is null', async () => {
useChromeMock.mockReturnValue({ auth: { getToken: () => Promise.resolve(null) } });
useAccessTokenBindingMock.mockReturnValue([{}, true]);
useFormikContextMock.mockReturnValue({
values: { source: { git: { url: 'https://github.com/test/repository' } }, secret: null },
values: { source: { git: { url: 'test-source' } } },
});
renderAuthOptions();
const button = screen.getByText('Sign in');
expect(button).toBeEnabled();

await act(async () => {
fireEvent.click(button);
});

expect(windowOpenMock).toHaveBeenCalledWith('example.com/auth?state=abcd', '_blank');
await waitFor(() => expect(screen.queryByText('Sign in')).not.toBeInTheDocument());
});

afterAll(jest.resetAllMocks);
Expand Down
8 changes: 7 additions & 1 deletion src/components/ImportForm/utils/auth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const useAccessCheck = (
] => {
const { namespace } = useWorkspaceInfo();
const [name, setName] = React.useState<string>();
const sourceRef = React.useRef(source);

React.useEffect(() => {
let resourceName = null;
Expand All @@ -48,6 +49,11 @@ export const useAccessCheck = (
setName(null);
return;
}

if (sourceRef.current !== source) {
sourceRef.current = source;

Check warning on line 54 in src/components/ImportForm/utils/auth-utils.ts

View check run for this annotation

Codecov / codecov/patch

src/components/ImportForm/utils/auth-utils.ts#L54

Added line #L54 was not covered by tests
}

if (source) {
k8sCreateResource<K8sResourceCommon>({
model: SPIAccessCheckModel,
Expand Down Expand Up @@ -84,7 +90,7 @@ export const useAccessCheck = (
}, [namespace, source, dependency]);

const [accessCheck, loaded] = useK8sWatchResource<SPIAccessCheckKind>(
name
name && sourceRef.current === source
? {
groupVersionKind: SPIAccessCheckGroupVersionKind,
name,
Expand Down

0 comments on commit 5ccfa07

Please sign in to comment.