Skip to content
Open
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
40 changes: 40 additions & 0 deletions apps/journeys-admin/__generated__/UserDeleteCheck.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions apps/journeys-admin/__generated__/UserDeleteConfirmSubscription.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/journeys-admin/__generated__/globalTypes.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions apps/journeys-admin/pages/api/clear-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextApiRequest, NextApiResponse } from 'next'

import { authConfig } from '../../src/libs/auth/config'

export default function handler(
_req: NextApiRequest,
res: NextApiResponse
): void {
res.setHeader(
'Set-Cookie',
`${authConfig.cookieName}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`
)
res.redirect(307, '/users/sign-in')
}
49 changes: 49 additions & 0 deletions apps/journeys-admin/pages/users/delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { GetServerSidePropsContext } from 'next'
import { useTranslation } from 'next-i18next'
import { NextSeo } from 'next-seo'
import { ReactElement } from 'react'

import { PageWrapper } from '../../src/components/PageWrapper'
import { UserDelete } from '../../src/components/UserDelete'
import { useAuth } from '../../src/libs/auth'
import {
getAuthTokens,
redirectToLogin,
toUser
} from '../../src/libs/auth/getAuthTokens'
import { initAndAuthApp } from '../../src/libs/initAndAuthApp'

export default function UserDeletePage(): ReactElement {
const { t } = useTranslation('apps-journeys-admin')
const { user } = useAuth()

return (
<>
<NextSeo title={t('Delete User')} />
<PageWrapper title={t('Delete User')} user={user ?? undefined}>
<UserDelete />
</PageWrapper>
</>
)
}

export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const tokens = await getAuthTokens(ctx)
if (tokens == null) return redirectToLogin(ctx)
const user = toUser(tokens)

const { redirect, translations } = await initAndAuthApp({
user,
locale: ctx.locale,
resolvedUrl: ctx.resolvedUrl
})

if (redirect != null) return { redirect }

return {
props: {
userSerialized: JSON.stringify(user),
...translations
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useTranslation } from 'next-i18next'
import { MouseEvent, ReactElement, useEffect, useState } from 'react'

import BoxIcon from '@core/shared/ui/icons/Box'
import Trash2Icon from '@core/shared/ui/icons/Trash2'
import UserProfile3Icon from '@core/shared/ui/icons/UserProfile3'

import { GetMe } from '../../../../../__generated__/GetMe'
Expand Down Expand Up @@ -147,6 +148,23 @@ export function UserNavigation({
/>
</ListItemButton>
)}
{data.me?.__typename === 'AuthenticatedUser' &&
data.me.superAdmin === true && (
<ListItemButton
LinkComponent={NextLink}
href="/users/delete"
selected={selectedPage === 'users-delete'}
data-testid="NavigationListItemDeleteUser"
>
<ListItemIcon>
<Trash2Icon />
</ListItemIcon>
<ListItemText
primary={t('Delete User')}
primaryTypographyProps={{ style: { whiteSpace: 'nowrap' } }}
/>
</ListItemButton>
)}
{data.me?.__typename === 'AuthenticatedUser' && (
<ListItemButton
data-testid="NavigationListItemProfile"
Expand Down
139 changes: 139 additions & 0 deletions apps/journeys-admin/src/components/UserDelete/UserDelete.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { MockedProvider } from '@apollo/client/testing'
import { render } from '@testing-library/react'

import { UserDeleteWithErrorBoundary } from './UserDelete'

const mockPush = jest.fn()

jest.mock('next/router', () => ({
useRouter: () => ({
push: mockPush,
query: {}
})
}))

jest.mock('notistack', () => ({
useSnackbar: () => ({
enqueueSnackbar: jest.fn()
})
}))

const mockUseSuspenseQuery = jest.fn()
jest.mock('@apollo/client', () => {
const actual = jest.requireActual('@apollo/client')
return {
...actual,
useSuspenseQuery: (...args: unknown[]) => mockUseSuspenseQuery(...args)
}
})

describe('UserDeleteWithErrorBoundary', () => {
beforeEach(() => {
jest.clearAllMocks()
mockUseSuspenseQuery.mockReturnValue({
data: {
me: {
__typename: 'AuthenticatedUser',
id: 'user-1',
superAdmin: true
}
}
})
})

it('should render the form for superAdmin users', () => {
const { getAllByText, getByText } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

expect(getAllByText('Delete User').length).toBeGreaterThanOrEqual(1)
expect(getByText('Check')).toBeInTheDocument()
expect(getByText('Warning')).toBeInTheDocument()
})

it('should redirect non-superAdmin users', () => {
mockUseSuspenseQuery.mockReturnValue({
data: {
me: {
__typename: 'AuthenticatedUser',
id: 'user-1',
superAdmin: false
}
}
})

render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

expect(mockPush).toHaveBeenCalledWith('/')
})

it('should render empty for non-superAdmin', () => {
mockUseSuspenseQuery.mockReturnValue({
data: {
me: {
__typename: 'AuthenticatedUser',
id: 'user-1',
superAdmin: false
}
}
})

const { queryByText } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

expect(queryByText('Check')).not.toBeInTheDocument()
})

it('should have delete button disabled before check', () => {
const { getAllByRole } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

const deleteUserButtons = getAllByRole('button', { name: 'Delete User' })
const actionBtn = deleteUserButtons[deleteUserButtons.length - 1]
expect(actionBtn).toBeDisabled()
})

it('should have check button disabled when input is empty', () => {
const { getByText } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

expect(getByText('Check').closest('button')).toBeDisabled()
})

it('should render lookup type selector with email as default', () => {
const { getByLabelText } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

expect(getByLabelText('Lookup By')).toBeInTheDocument()
})

it('should render logs textfield', () => {
const { getByRole } = render(
<MockedProvider>
<UserDeleteWithErrorBoundary />
</MockedProvider>
)

const logsField = getByRole('textbox', { name: 'Logs' })
expect(logsField).toBeInTheDocument()
expect(logsField).toHaveAttribute('readonly')
})
})
Loading
Loading