Skip to content

Commit

Permalink
feat: Render okta errors based on error param (#3196)
Browse files Browse the repository at this point in the history
  • Loading branch information
RulaKhaled committed Sep 16, 2024
1 parent 1f8f915 commit 8749dcc
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 4 deletions.
43 changes: 43 additions & 0 deletions src/shared/GlobalTopBanners/OktaBanners/OktaBanners.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import OktaBanners from './OktaBanners'

jest.mock('../OktaEnabledBanner', () => () => 'OktaEnabledBanner')
jest.mock('../OktaEnforcedBanner', () => () => 'OktaEnforcedBanner')
jest.mock('../OktaErrorBanners', () => () => 'OktaErrorBanners')

const wrapper =
(initialEntries = ['/gh/owner']): React.FC<React.PropsWithChildren> =>
Expand Down Expand Up @@ -145,6 +146,27 @@ describe('OktaBanners', () => {
const banner = await screen.findByText(/OktaEnabledBanner/)
expect(banner).toBeInTheDocument()
})

it('should render OktaErrorBanners', async () => {
setup({
owner: {
isUserOktaAuthenticated: false,
account: {
oktaConfig: {
enabled: true,
enforced: false,
url: 'https://okta.com',
clientId: 'clientId',
clientSecret: 'clientSecret',
},
},
},
})

render(<OktaBanners />, { wrapper: wrapper() })
const banner = await screen.findByText(/OktaErrorBanners/)
expect(banner).toBeInTheDocument()
})
})

describe('when Okta is enforced', () => {
Expand All @@ -169,6 +191,27 @@ describe('OktaBanners', () => {
const banner = await screen.findByText(/OktaEnforcedBanner/)
expect(banner).toBeInTheDocument()
})

it('should render OktaErrorBanners', async () => {
setup({
owner: {
isUserOktaAuthenticated: false,
account: {
oktaConfig: {
enabled: true,
enforced: true,
url: 'https://okta.com',
clientId: 'clientId',
clientSecret: 'clientSecret',
},
},
},
})

render(<OktaBanners />, { wrapper: wrapper() })
const banner = await screen.findByText(/OktaErrorBanners/)
expect(banner).toBeInTheDocument()
})
})
})
})
Expand Down
8 changes: 7 additions & 1 deletion src/shared/GlobalTopBanners/OktaBanners/OktaBanners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface URLParams {

const OktaEnabledBanner = lazy(() => import('../OktaEnabledBanner'))
const OktaEnforcedBanner = lazy(() => import('../OktaEnforcedBanner'))
const OktaErrorBanners = lazy(() => import('../OktaErrorBanners'))

function OktaBanners() {
const { provider, owner } = useParams<URLParams>()
Expand All @@ -25,7 +26,12 @@ function OktaBanners() {
if (!owner || !oktaConfig?.enabled || data?.owner?.isUserOktaAuthenticated)
return null

return oktaConfig?.enforced ? <OktaEnforcedBanner /> : <OktaEnabledBanner />
return (
<div className="flex flex-col gap-2">
{oktaConfig?.enforced ? <OktaEnforcedBanner /> : <OktaEnabledBanner />}
<OktaErrorBanners />
</div>
)
}

export default OktaBanners
111 changes: 111 additions & 0 deletions src/shared/GlobalTopBanners/OktaErrorBanners/OktaErrorBanners.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as Sentry from '@sentry/react'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route } from 'react-router-dom'

import OktaErrorBanners from './OktaErrorBanners'

jest.mock('@sentry/react', () => {
const originalModule = jest.requireActual('@sentry/react')
return {
...originalModule,
captureMessage: jest.fn(),
}
})

const wrapper =
(
initialEntries = ['/gh/codecov'],
path = '/:provider/:owner'
): React.FC<React.PropsWithChildren> =>
({ children }) => (
<MemoryRouter initialEntries={initialEntries}>
<Route path={path}>{children}</Route>
</MemoryRouter>
)

describe('OktaErrorBanners', () => {
it('should return null if owner is not provided', () => {
const { container } = render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/'], '/:provider'),
})

expect(container).toBeEmptyDOMElement()
})

it('should return null if error is not provided', () => {
const { container } = render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov']),
})

expect(container).toBeEmptyDOMElement()
})

it('should render error message for invalid_request', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=invalid_request']),
})

const content = screen.getByText(
/Invalid request: The request parameters aren't valid. Please try again or contact support./
)
expect(content).toBeInTheDocument()
})

it('should render error message for unauthorized_client', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=unauthorized_client']),
})

const content = screen.getByText(
/Unauthorized client: The client isn't authorized to request an authorization code using this method. Please reach out to your administrator./
)
expect(content).toBeInTheDocument()
})

it('should render error message for access_denied', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=access_denied']),
})

const content = screen.getByText(
/The resource owner or authorization server denied the request/
)
expect(content).toBeInTheDocument()
})

it('should render default error message for unknown error', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=unknown']),
})

const content = screen.getByText(
/An unknown error occurred. Please try again or contact support./
)
expect(content).toBeInTheDocument()
})

it('should capture unknown error message', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=unknown']),
})

expect(Sentry.captureMessage).toHaveBeenCalledWith(
'Unknown Okta error: unknown',
{
fingerprint: ['unknown-okta-error'],
tags: {
error: 'unknown',
},
}
)
})

it('should render dismiss button', () => {
render(<OktaErrorBanners />, {
wrapper: wrapper(['/gh/codecov?error=invalid_request']),
})

const dismissButton = screen.getByRole('button', { name: /Dismiss/ })
expect(dismissButton).toBeInTheDocument()
})
})
41 changes: 41 additions & 0 deletions src/shared/GlobalTopBanners/OktaErrorBanners/OktaErrorBanners.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useLocation, useParams } from 'react-router-dom'

import Icon from 'ui/Icon'
import TopBanner from 'ui/TopBanner'

import { getOktaErrorMessage } from './enums'

interface URLParams {
owner?: string
}

const OktaErrorBanners = () => {
const { owner } = useParams<URLParams>()
const location = useLocation()

const searchParams = new URLSearchParams(location.search)
const error = searchParams.get('error')

if (!owner || !error) return null

const errorMessage = getOktaErrorMessage(error)

return (
<TopBanner variant="error">
<TopBanner.Start>
<p className="items-center gap-1 md:flex">
<span className="flex items-center gap-1 font-semibold">
<Icon name="exclamationCircle" />
Okta Authentication Error
</span>
{errorMessage}
</p>
</TopBanner.Start>
<TopBanner.End>
<TopBanner.DismissButton>Dismiss</TopBanner.DismissButton>
</TopBanner.End>
</TopBanner>
)
}

export default OktaErrorBanners
55 changes: 55 additions & 0 deletions src/shared/GlobalTopBanners/OktaErrorBanners/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as Sentry from '@sentry/react'

const ErrorType = {
UnauthorizedClient: 'unauthorized_client',
AccessDenied: 'access_denied',
UnsupportedResponseType: 'unsupported_response_type',
UnsupportedResponseMode: 'unsupported_response_mode',
InvalidScope: 'invalid_scope',
ServerError: 'server_error',
TemporarilyUnavailable: 'temporarily_unavailable',
InvalidClient: 'invalid_client',
LoginRequired: 'login_required',
InvalidRequest: 'invalid_request',
UserCanceledRequest: 'user_canceled_request',
} as const

const errorMessages = {
[ErrorType.UnauthorizedClient]:
"Unauthorized client: The client isn't authorized to request an authorization code using this method. Please reach out to your administrator.",
[ErrorType.AccessDenied]:
'Access denied: The resource owner or authorization server denied the request. Please reach out to your administrator.',
[ErrorType.UnsupportedResponseType]:
"Unsupported response type: The authorization server doesn't support obtaining an authorization code using this method. Please reach out to your administrator.",
[ErrorType.UnsupportedResponseMode]:
"Unsupported response mode: The authorization server doesn't support the requested response mode. Please reach out to your administrator.",
[ErrorType.InvalidScope]:
'Invalid scope: The requested scope is invalid, unknown, or malformed. Please reach out to your administrator.',
[ErrorType.ServerError]:
'Server error: The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Please try again later or contact support.',
[ErrorType.TemporarilyUnavailable]:
'Temporarily unavailable: The authorization server is currently unable to handle the request due to temporary overloading or maintenance. Please try again later.',
[ErrorType.InvalidClient]:
"Invalid client: The specified client isn't valid. Please reach out to your administrator.",
[ErrorType.LoginRequired]:
"Login required: The client specified not to prompt, but the user isn't signed in. Please sign in and try again.",
[ErrorType.InvalidRequest]:
"Invalid request: The request parameters aren't valid. Please try again or contact support.",
[ErrorType.UserCanceledRequest]:
'Request canceled: User canceled the social sign-in request. Please try again if this was unintentional.',
} as const

export const getOktaErrorMessage = (error: string): string => {
if (error in errorMessages) {
return errorMessages[error as keyof typeof errorMessages]
}

Sentry.captureMessage(`Unknown Okta error: ${error}`, {
fingerprint: ['unknown-okta-error'],
tags: {
error: error,
},
})

return 'An unknown error occurred. Please try again or contact support.'
}
1 change: 1 addition & 0 deletions src/shared/GlobalTopBanners/OktaErrorBanners/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './OktaErrorBanners'
4 changes: 2 additions & 2 deletions src/ui/TopBanner/TopBanner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const meta: Meta<typeof TopBannerComponent> = {
},
},
variant: {
options: ['default', 'warning'],
options: ['default', 'warning', 'error'],
control: 'radio',
description:
'This prop controls the variation of top banner that is displayed',
Expand All @@ -35,7 +35,7 @@ export const TopBanner: Story = {
},
argTypes: {
variant: {
options: ['default', 'warning'],
options: ['default', 'warning', 'error'],
control: 'radio',
},
children: {
Expand Down
11 changes: 10 additions & 1 deletion src/ui/TopBanner/TopBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ const variants = {
iconColor: 'text-ds-primary-yellow',
bgColor: 'bg-orange-100',
},
error: {
icon: 'exclamationCircle',
iconColor: '',
bgColor: 'bg-ds-primary-red',
},
} as const

type Variants = keyof typeof variants

const topBannerContext = z.object({
variant: z.union([z.literal('default'), z.literal('warning')]),
variant: z.union([
z.literal('default'),
z.literal('warning'),
z.literal('error'),
]),
localStorageKey: z.string().optional(),
setHideBanner: z.function().args(z.boolean()).returns(z.void()),
})
Expand Down

0 comments on commit 8749dcc

Please sign in to comment.