Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Tokenless section in global upload token tab #3223

Merged
merged 8 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { lazy } from 'react'
import { useParams } from 'react-router-dom'

import { useOrgUploadToken } from 'services/orgUploadToken'
import { useFlags } from 'shared/featureFlags'
import A from 'ui/A'
import Banner from 'ui/Banner'

import GenerateOrgUploadToken from './GenerateOrgUploadToken'
import RegenerateOrgUploadToken from './RegenerateOrgUploadToken'

const TokenlessSection = lazy(() => import('./TokenlessSection'))

function OrgUploadToken() {
const { provider, owner } = useParams()
const { data: orgUploadToken } = useOrgUploadToken({ provider, owner })
const { tokenlessSection: tokenlessSectionFlag } = useFlags({
tokenlessSection: false,
})

return (
<div className="flex flex-col gap-4 lg:w-3/4">
Expand All @@ -21,6 +28,7 @@ function OrgUploadToken() {
</div>
<hr />
<div className="flex flex-col gap-6">
{tokenlessSectionFlag ? <TokenlessSection /> : null}
<Banner>
<h2 className="font-semibold">Sensitive credential</h2>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { Suspense } from 'react'
import { MemoryRouter, Route } from 'react-router-dom'

import { useAddNotification } from 'services/toastNotification'
import { useFlags } from 'shared/featureFlags'

import OrgUploadToken from './OrgUploadToken'

vi.mock('services/toastNotification')
vi.mock('shared/featureFlags')
vi.mock('./TokenlessSection', () => ({ default: () => 'TokenlessSection' }))

const mockOwner = {
owner: {
Expand Down Expand Up @@ -60,6 +63,7 @@ describe('OrgUploadToken', () => {
const user = userEvent.setup()
const mutate = vi.fn()
const addNotification = vi.fn()
useFlags.mockReturnValue({ tokenlessSection: true })

useAddNotification.mockReturnValue(addNotification)

Expand Down Expand Up @@ -147,6 +151,14 @@ describe('OrgUploadToken', () => {
'https://docs.codecov.com/docs/codecov-uploader#organization-upload-token'
)
})

it('renders TokenlessSection component', async () => {
setup({ orgUploadToken: 'upload-token' })
render(<OrgUploadToken />, { wrapper })

const tokenlessSection = await screen.findByText('TokenlessSection')
expect(tokenlessSection).toBeInTheDocument()
})
})

describe('when user clicks on Generate button', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { render, screen, waitFor } from 'custom-testing-library'

import userEvent from '@testing-library/user-event'
import { MemoryRouter, Route } from 'react-router-dom'

import TokenRequiredModal from './TokenRequiredModal'

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<MemoryRouter initialEntries={['/some-initial-path']}>
<Route path="/some-initial-path">{children}</Route>
</MemoryRouter>
)

describe('TokenRequiredModal', () => {
function setup() {
return {
user: userEvent.setup(),
closeModal: jest.fn(),
setTokenRequired: jest.fn(),
}
}

describe('renders the modal content correctly', () => {
it('renders the correct modal title', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const title = await screen.findByText(/Require token for uploads/)
expect(title).toBeInTheDocument()
})

it('renders the correct body content part 1', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const messagePartOne = await screen.findByText(
/Enforcing token authentication for uploads/
)
expect(messagePartOne).toBeInTheDocument()
})

it('renders the correct body content part 2', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const messagePartTwo = await screen.findByText(
/Before proceeding, make sure all of your repositories/
)
expect(messagePartTwo).toBeInTheDocument()
})

it('renders the correct body content part 3', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const messagePartThree = await screen.findByText(
/to enforce the use of the global token for uploads./
)
expect(messagePartThree).toBeInTheDocument()
})

it('renders the cancel button', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const cancelButton = await screen.findByRole('button', { name: /Cancel/ })
expect(cancelButton).toBeInTheDocument()
})

it('renders the require token button', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const requireButton = await screen.findByRole('button', {
name: /Require token for upload/,
})
expect(requireButton).toBeInTheDocument()
})

it('renders loading state on require token button', async () => {
const { closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={true}
/>,
{ wrapper }
)

const requireButton = await screen.findByRole('button', {
name: /Require token for upload/,
})
expect(requireButton).toHaveAttribute('disabled')
})
})

describe('when clicking cancel button', () => {
it('closes the modal without requiring token', async () => {
const { user, closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const cancelButton = await screen.findByRole('button', { name: /Cancel/ })
await user.click(cancelButton)

expect(setTokenRequired).toHaveBeenCalledWith(false)
await waitFor(() => expect(closeModal).toHaveBeenCalled())
})
})

describe('when clicking require token button', () => {
it('sets token requirement and closes the modal', async () => {
const { user, closeModal, setTokenRequired } = setup()

render(
<TokenRequiredModal
closeModal={closeModal}
setTokenRequired={setTokenRequired}
isLoading={false}
/>,
{ wrapper }
)

const requireButton = await screen.findByRole('button', {
name: /Require token for upload/,
})
await user.click(requireButton)

expect(setTokenRequired).toHaveBeenCalledWith(true)
await waitFor(() => expect(closeModal).toHaveBeenCalled())
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Button from 'ui/Button'
import Icon from 'ui/Icon'
import Modal from 'ui/Modal'

interface TokenlessModalProps {
closeModal: () => void
setTokenRequired: (value: boolean) => void
isLoading: boolean
}

const TokenlessModal = ({
closeModal,
setTokenRequired,
isLoading,
}: TokenlessModalProps) => (
<Modal
isOpen={true}
onClose={closeModal}
title={
<p className="flex items-center gap-2 text-base">
<Icon
name="exclamationTriangle"
size="sm"
className="fill-ds-primary-yellow"
/>
Require token for uploads
</p>
}
body={
<div className="flex flex-col gap-4">
<p>
Enforcing token authentication for uploads within your organization
will cause uploads without a token to be rejected for all of your
repositories.
</p>
<p>
Before proceeding, make sure all of your repositories can access
either your global upload token or a repository-specific token in your
CI configuration and that your CI jobs are using one of them when
submitting uploads.
</p>
<p>
Click <span className="font-semibold">Require token for upload</span>{' '}
to enforce the use of the global token for uploads.
</p>
</div>
}
footer={
<div className="flex gap-2">
<Button
hook="cancel-token-requirement"
onClick={() => {
setTokenRequired(false)
closeModal()
}}
to={undefined}
disabled={undefined}
>
Cancel
</Button>
<Button
isLoading={isLoading}
hook="require-token-upload"
variant="primary"
onClick={() => {
setTokenRequired(true)
closeModal()
}}
to={undefined}
disabled={undefined}
>
Require token for upload
</Button>
</div>
}
/>
)

export default TokenlessModal
Loading
Loading