Skip to content

Commit 67c1a28

Browse files
Add support for OCR2 Key Management - No creation for now (#26)
Co-authored-by: george-dorin <[email protected]>
1 parent 6917e8a commit 67c1a28

File tree

12 files changed

+646
-0
lines changed

12 files changed

+646
-0
lines changed

.changeset/twenty-radios-tease.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@smartcontractkit/operator-ui': minor
3+
---
4+
5+
Added support for the display and deletion of OCR version 2 (OCR2) keys

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ tsconfig.tsbuildinfo
88
.npmrc
99
assets
1010
yarn-error.log
11+
12+
# OS specific
13+
.DS_Store
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react'
2+
3+
import { ApolloError } from '@apollo/client'
4+
import { GraphQLError } from 'graphql'
5+
import { Route } from 'react-router-dom'
6+
import { renderWithRouter, screen } from 'support/test-utils'
7+
import { getAuthentication } from 'utils/storage'
8+
9+
import Notifications from 'pages/Notifications'
10+
import { useQueryErrorHandler } from 'hooks/useQueryErrorHandler'
11+
12+
const { getByText } = screen
13+
14+
const StubComponent = ({ mockError }: { mockError?: unknown }) => {
15+
const { handleQueryError } = useQueryErrorHandler()
16+
17+
React.useEffect(() => {
18+
handleQueryError(mockError)
19+
}, [mockError, handleQueryError])
20+
21+
return null
22+
}
23+
24+
function renderComponent(mockError?: unknown) {
25+
renderWithRouter(
26+
<>
27+
<Notifications />
28+
<Route exact path="/">
29+
<StubComponent mockError={mockError} />
30+
</Route>
31+
32+
<Route exact path="/signin">
33+
Redirect Success
34+
</Route>
35+
</>,
36+
)
37+
}
38+
39+
describe('useQueryErrorHandler', () => {
40+
it('renders an empty component if error undefined', () => {
41+
renderComponent()
42+
43+
expect(document.documentElement).toHaveTextContent('')
44+
})
45+
46+
it('renders the apollo error message', () => {
47+
const graphQLErrors = [new GraphQLError('GraphQL error')]
48+
const errorMessage = 'Something went wrong'
49+
const apolloError = new ApolloError({
50+
graphQLErrors,
51+
errorMessage,
52+
})
53+
54+
renderComponent(apolloError)
55+
56+
expect(getByText('Something went wrong')).toBeInTheDocument()
57+
})
58+
59+
it('redirects an authenticated error', () => {
60+
const graphQLErrors = [
61+
new GraphQLError(
62+
'Unauthorized',
63+
undefined,
64+
undefined,
65+
undefined,
66+
undefined,
67+
undefined,
68+
{ code: 'UNAUTHORIZED' },
69+
),
70+
]
71+
const errorMessage = 'Something went wrong'
72+
const apolloError = new ApolloError({
73+
graphQLErrors,
74+
errorMessage,
75+
})
76+
77+
renderComponent(apolloError)
78+
79+
expect(getByText('Redirect Success')).toBeInTheDocument()
80+
expect(getAuthentication()).toEqual({ allowed: false })
81+
})
82+
83+
it('renders the message in an alert when it is a simple error', () => {
84+
renderComponent(new Error('Something went wrong'))
85+
86+
expect(getByText('Something went wrong')).toBeInTheDocument()
87+
})
88+
89+
it('renders a generic message in an alert as a default', () => {
90+
renderComponent('generic message') // A string type is not handled and falls to the default
91+
92+
expect(getByText('An error occurred')).toBeInTheDocument()
93+
})
94+
})

src/hooks/useQueryErrorHandler.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react'
2+
import { useHistory } from 'react-router-dom'
3+
import { useDispatch } from 'react-redux'
4+
5+
import { notifyErrorMsg } from 'actionCreators'
6+
import { ApolloError } from '@apollo/client'
7+
import { receiveSignoutSuccess } from 'actionCreators'
8+
/**
9+
* Handles an unknown error which is caught from a
10+
* query operation. If the error returned is an authentication error, it
11+
* signs the user out and redirects them to the sign-in page, otherwise it
12+
* displays an alert with the error message.
13+
*/
14+
export const useQueryErrorHandler = () => {
15+
const [error, handleQueryError] = React.useState<unknown>()
16+
const history = useHistory()
17+
const dispatch = useDispatch()
18+
19+
React.useEffect(() => {
20+
if (!error) {
21+
return
22+
}
23+
24+
if (error instanceof ApolloError) {
25+
// Check for an authentication error and logout
26+
for (const gqlError of error.graphQLErrors) {
27+
if (gqlError.extensions?.code == 'UNAUTHORIZED') {
28+
dispatch(
29+
notifyErrorMsg(
30+
'Unauthorized, please log in with proper credentials',
31+
),
32+
)
33+
dispatch(receiveSignoutSuccess())
34+
history.push('/signin')
35+
36+
return
37+
}
38+
}
39+
}
40+
dispatch(notifyErrorMsg((error as Error).message || 'An error occurred'))
41+
}, [dispatch, error, history])
42+
43+
return { handleQueryError }
44+
}

src/screens/KeyManagement/KeyManagementView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Content from 'components/Content'
66
import { EVMAccounts } from './EVMAccounts'
77
import { CSAKeys } from './CSAKeys'
88
import { OCRKeys } from './OCRKeys'
9+
import { OCR2Keys } from './OCR2Keys'
910
import { P2PKeys } from './P2PKeys'
1011

1112
interface Props {
@@ -22,6 +23,10 @@ export const KeyManagementView: React.FC<Props> = ({
2223
<OCRKeys />
2324
</Grid>
2425

26+
<Grid item xs={12}>
27+
<OCR2Keys />
28+
</Grid>
29+
2530
<Grid item xs={12}>
2631
<P2PKeys />
2732
</Grid>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from 'react'
2+
3+
import { render, screen } from 'support/test-utils'
4+
5+
import { buildOCR2KeyBundle } from 'support/factories/gql/fetchOCR2KeyBundles'
6+
import { OCR2KeyBundleRow } from './OCR2KeyBundleRow'
7+
import userEvent from '@testing-library/user-event'
8+
9+
const { getByRole, queryByText } = screen
10+
11+
describe('OCR2KeyBundleRow', () => {
12+
let handleDelete: jest.Mock
13+
14+
beforeEach(() => {
15+
handleDelete = jest.fn()
16+
})
17+
18+
function renderComponent(bundle: Ocr2KeyBundlesPayload_ResultsFields) {
19+
render(
20+
<table>
21+
<tbody>
22+
<OCR2KeyBundleRow bundle={bundle} onDelete={handleDelete} />
23+
</tbody>
24+
</table>,
25+
)
26+
}
27+
28+
it('renders a row', () => {
29+
const bundle = buildOCR2KeyBundle()
30+
31+
renderComponent(bundle)
32+
33+
expect(queryByText(`Key ID: ${bundle.id}`)).toBeInTheDocument()
34+
expect(
35+
queryByText(`Chain Type: ${bundle.chainType}`),
36+
).toBeInTheDocument()
37+
expect(
38+
queryByText(`Config Public Key: ${bundle.configPublicKey}`),
39+
).toBeInTheDocument()
40+
expect(
41+
queryByText(`On-Chain Public Key: ${bundle.onChainPublicKey}`),
42+
).toBeInTheDocument()
43+
expect(
44+
queryByText(`Off-Chain Public Key: ${bundle.offChainPublicKey}`),
45+
).toBeInTheDocument()
46+
})
47+
48+
it('calls delete', () => {
49+
const bundle = buildOCR2KeyBundle()
50+
51+
renderComponent(bundle)
52+
53+
userEvent.click(getByRole('button', { name: /delete/i }))
54+
55+
expect(handleDelete).toHaveBeenCalled()
56+
})
57+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react'
2+
3+
import Button from 'src/components/Button'
4+
import TableCell from '@material-ui/core/TableCell'
5+
import TableRow from '@material-ui/core/TableRow'
6+
7+
import { KeyBundle } from './KeyBundle'
8+
import { CopyIconButton } from 'src/components/Copy/CopyIconButton'
9+
10+
interface Props {
11+
bundle: Ocr2KeyBundlesPayload_ResultsFields
12+
onDelete: () => void
13+
}
14+
15+
/**
16+
* This row follows the form and structure of OCRKeyBundleRow but
17+
* uses the new data for keys from OCR2
18+
*/
19+
export const OCR2KeyBundleRow: React.FC<Props> = ({ bundle, onDelete }) => {
20+
return (
21+
<TableRow hover>
22+
<TableCell>
23+
<KeyBundle
24+
primary={
25+
<b>
26+
Key ID: {bundle.id} <CopyIconButton data={bundle.id} />
27+
</b>
28+
}
29+
secondary={[
30+
<>Chain Type: {bundle.chainType}</>,
31+
<>Config Public Key: {bundle.configPublicKey}</>,
32+
<>On-Chain Public Key: {bundle.onChainPublicKey}</>,
33+
<>Off-Chain Public Key: {bundle.offChainPublicKey}</>,
34+
]}
35+
/>
36+
</TableCell>
37+
<TableCell align="right">
38+
<Button onClick={onDelete} variant="danger" size="medium">
39+
Delete
40+
</Button>
41+
</TableCell>
42+
</TableRow>
43+
)
44+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as React from 'react'
2+
3+
import { GraphQLError } from 'graphql'
4+
import {
5+
renderWithRouter,
6+
screen,
7+
waitForElementToBeRemoved,
8+
} from 'support/test-utils'
9+
import { MockedProvider, MockedResponse } from '@apollo/client/testing'
10+
import userEvent from '@testing-library/user-event'
11+
12+
import {
13+
OCR2Keys,
14+
DELETE_OCR2_KEY_BUNDLE_MUTATION,
15+
} from './OCR2Keys'
16+
import {
17+
buildOCR2KeyBundle,
18+
buildOCR2KeyBundles,
19+
} from 'support/factories/gql/fetchOCR2KeyBundles'
20+
import Notifications from 'pages/Notifications'
21+
import { OCR2_KEY_BUNDLES_QUERY } from 'src/hooks/queries/useOCR2KeysQuery'
22+
import { waitForLoading } from 'support/test-helpers/wait'
23+
24+
const { findByText, getByRole, queryByText } = screen
25+
26+
function renderComponent(mocks: MockedResponse[]) {
27+
renderWithRouter(
28+
<>
29+
<Notifications />
30+
<MockedProvider mocks={mocks} addTypename={false}>
31+
<OCR2Keys />
32+
</MockedProvider>
33+
</>,
34+
)
35+
}
36+
37+
function fetchOCR2KeyBundlesQuery(
38+
bundles: ReadonlyArray<Ocr2KeyBundlesPayload_ResultsFields>,
39+
) {
40+
return {
41+
request: {
42+
query: OCR2_KEY_BUNDLES_QUERY,
43+
},
44+
result: {
45+
data: {
46+
ocr2KeyBundles: {
47+
results: bundles,
48+
},
49+
},
50+
},
51+
}
52+
}
53+
54+
describe('OCR2Keys', () => {
55+
it('renders the page', async () => {
56+
const payload = buildOCR2KeyBundles()
57+
const mocks: MockedResponse[] = [fetchOCR2KeyBundlesQuery(payload)]
58+
59+
renderComponent(mocks)
60+
61+
await waitForLoading()
62+
63+
expect(await findByText(`Key ID: ${payload[0].id}`)).toBeInTheDocument()
64+
})
65+
66+
it('renders GQL query errors', async () => {
67+
const mocks: MockedResponse[] = [
68+
{
69+
request: {
70+
query: OCR2_KEY_BUNDLES_QUERY,
71+
},
72+
result: {
73+
errors: [new GraphQLError('Error!')],
74+
},
75+
},
76+
]
77+
78+
renderComponent(mocks)
79+
80+
expect(await findByText('Error!')).toBeInTheDocument()
81+
})
82+
83+
it('deletes an OCR2 Key Bundle', async () => {
84+
const payload = buildOCR2KeyBundle()
85+
86+
const mocks: MockedResponse[] = [
87+
fetchOCR2KeyBundlesQuery([payload]),
88+
{
89+
request: {
90+
query: DELETE_OCR2_KEY_BUNDLE_MUTATION,
91+
variables: { id: payload.id },
92+
},
93+
result: {
94+
data: {
95+
deleteOCR2KeyBundle: {
96+
__typename: 'DeleteOCR2KeyBundleSuccess',
97+
bundle: payload,
98+
},
99+
},
100+
},
101+
},
102+
fetchOCR2KeyBundlesQuery([]),
103+
]
104+
105+
renderComponent(mocks)
106+
107+
expect(await findByText(`Key ID: ${payload.id}`)).toBeInTheDocument()
108+
109+
userEvent.click(getByRole('button', { name: /delete/i }))
110+
userEvent.click(getByRole('button', { name: /confirm/i }))
111+
112+
await waitForElementToBeRemoved(getByRole('dialog'))
113+
114+
expect(
115+
await findByText(
116+
'Successfully deleted Off-ChainReporting Key Bundle Key',
117+
),
118+
).toBeInTheDocument()
119+
120+
expect(queryByText(`Key ID: ${payload.id}`)).toBeNull()
121+
})
122+
})

0 commit comments

Comments
 (0)