-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨(frontend) add mail domain access management
- access management view is ready to use get, patch and delete requests once backend is ready. How to create accesses with post will come later in a future commit. - update translations and component tests. - reduce gap between mail domains feature logo and mail domain name in top banner
- Loading branch information
1 parent
cffce3a
commit a14866e
Showing
30 changed files
with
2,593 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
...rontend/apps/desk/src/features/mail-domains/access-management/__tests__/accesses.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import fetchMock from 'fetch-mock'; | ||
import { useRouter as useNavigate } from 'next/navigation'; | ||
import { useRouter } from 'next/router'; | ||
|
||
import { AccessesContent } from '@/features/mail-domains/access-management'; | ||
import { Role } from '@/features/mail-domains/domains'; | ||
import MailDomainAccessesPage from '@/pages/mail-domains/[slug]/accesses'; | ||
import { AppWrapper } from '@/tests/utils'; | ||
|
||
jest.mock('next/navigation', () => ({ | ||
useRouter: jest.fn(), | ||
})); | ||
|
||
jest.mock('next/router', () => ({ | ||
useRouter: jest.fn(), | ||
})); | ||
|
||
jest.mock( | ||
'@/features/mail-domains/access-management/components/AccessesContent', | ||
() => ({ | ||
AccessesContent: jest.fn(() => <div>AccessContent</div>), | ||
}), | ||
); | ||
|
||
describe('MailDomainAccessesPage', () => { | ||
const mockRouterReplace = jest.fn(); | ||
const mockNavigate = { replace: mockRouterReplace }; | ||
const mockRouter = { | ||
query: { slug: 'example-slug' }, | ||
}; | ||
|
||
(useRouter as jest.Mock).mockReturnValue(mockRouter); | ||
(useNavigate as jest.Mock).mockReturnValue(mockNavigate); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
fetchMock.reset(); | ||
(useRouter as jest.Mock).mockReturnValue(mockRouter); | ||
(useNavigate as jest.Mock).mockReturnValue(mockNavigate); | ||
}); | ||
|
||
afterEach(() => { | ||
fetchMock.restore(); | ||
}); | ||
|
||
const renderPage = () => { | ||
render(<MailDomainAccessesPage />, { wrapper: AppWrapper }); | ||
}; | ||
|
||
it('renders loader while loading', () => { | ||
// Simulate a never-resolving promise to mock loading | ||
fetchMock.mock( | ||
`end:/mail-domains/${mockRouter.query.slug}/`, | ||
new Promise(() => {}), | ||
); | ||
|
||
renderPage(); | ||
|
||
expect(screen.getByRole('status')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders error message when there is an error', async () => { | ||
fetchMock.mock(`end:/mail-domains/${mockRouter.query.slug}/`, { | ||
status: 500, | ||
}); | ||
|
||
renderPage(); | ||
|
||
await waitFor(() => { | ||
expect( | ||
screen.getByText('Something bad happens, please retry.'), | ||
).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('redirects to 404 page if the domain is not found', async () => { | ||
fetchMock.mock( | ||
`end:/mail-domains/${mockRouter.query.slug}/`, | ||
{ | ||
body: { detail: 'Not found' }, | ||
status: 404, | ||
}, | ||
{ overwriteRoutes: true }, | ||
); | ||
|
||
renderPage(); | ||
|
||
await waitFor(() => { | ||
expect(mockRouterReplace).toHaveBeenCalledWith('/404'); | ||
}); | ||
}); | ||
|
||
it('renders the AccessesContent when data is available', async () => { | ||
const mockMailDomain = { | ||
id: '1-1-1-1-1', | ||
name: 'example.com', | ||
slug: 'example-com', | ||
status: 'enabled', | ||
created_at: new Date().toISOString(), | ||
updated_at: new Date().toISOString(), | ||
abilities: { | ||
get: true, | ||
patch: true, | ||
put: true, | ||
post: true, | ||
delete: true, | ||
manage_accesses: true, | ||
}, | ||
}; | ||
|
||
fetchMock.mock(`end:/mail-domains/${mockRouter.query.slug}/`, { | ||
body: mockMailDomain, | ||
status: 200, | ||
}); | ||
|
||
renderPage(); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('AccessContent')).toBeInTheDocument(); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(AccessesContent).toHaveBeenCalledWith( | ||
{ | ||
mailDomain: mockMailDomain, | ||
currentRole: Role.OWNER, | ||
}, | ||
{}, // adding this empty object is necessary to load jest context | ||
); | ||
}); | ||
}); | ||
|
||
it('throws an error when slug is invalid', () => { | ||
console.error = jest.fn(); // Suppress expected error in jest logs | ||
|
||
(useRouter as jest.Mock).mockReturnValue({ | ||
query: { slug: ['invalid-array-slug-in-array'] }, | ||
}); | ||
|
||
expect(() => renderPage()).toThrow('Invalid mail domain slug'); | ||
}); | ||
}); |
109 changes: 109 additions & 0 deletions
109
.../features/mail-domains/access-management/api/__tests__/useDeleteMailDomainAccess.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { renderHook, waitFor } from '@testing-library/react'; | ||
import fetchMock from 'fetch-mock'; | ||
|
||
import { APIError } from '@/api'; | ||
import { AppWrapper } from '@/tests/utils'; | ||
|
||
import { | ||
deleteMailDomainAccess, | ||
useDeleteMailDomainAccess, | ||
} from '../useDeleteMailDomainAccess'; | ||
|
||
describe('deleteMailDomainAccess', () => { | ||
afterEach(() => { | ||
fetchMock.restore(); | ||
}); | ||
|
||
it('deletes the access successfully', async () => { | ||
fetchMock.deleteOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', { | ||
status: 204, // No content status | ||
}); | ||
|
||
await deleteMailDomainAccess({ | ||
slug: 'example-slug', | ||
accessId: '1-1-1-1-1', | ||
}); | ||
|
||
expect(fetchMock.calls()).toHaveLength(1); | ||
expect(fetchMock.lastUrl()).toContain( | ||
'/mail-domains/example-slug/accesses/1-1-1-1-1/', | ||
); | ||
}); | ||
|
||
it('throws an error when the API call fails', async () => { | ||
fetchMock.deleteOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', { | ||
status: 500, | ||
body: { cause: ['Internal server error'] }, | ||
}); | ||
|
||
await expect( | ||
deleteMailDomainAccess({ | ||
slug: 'example-slug', | ||
accessId: '1-1-1-1-1', | ||
}), | ||
).rejects.toThrow(APIError); | ||
expect(fetchMock.calls()).toHaveLength(1); | ||
}); | ||
}); | ||
|
||
describe('useDeleteMailDomainAccess', () => { | ||
afterEach(() => { | ||
fetchMock.restore(); | ||
}); | ||
|
||
it('deletes the access and calls onSuccess callback', async () => { | ||
fetchMock.deleteOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', { | ||
status: 204, // No content status | ||
}); | ||
|
||
const onSuccess = jest.fn(); | ||
|
||
const { result } = renderHook( | ||
() => useDeleteMailDomainAccess({ onSuccess }), | ||
{ | ||
wrapper: AppWrapper, | ||
}, | ||
); | ||
|
||
result.current.mutate({ | ||
slug: 'example-slug', | ||
accessId: '1-1-1-1-1', | ||
}); | ||
|
||
await waitFor(() => expect(fetchMock.calls()).toHaveLength(1)); | ||
await waitFor(() => | ||
expect(onSuccess).toHaveBeenCalledWith( | ||
undefined, | ||
{ slug: 'example-slug', accessId: '1-1-1-1-1' }, | ||
undefined, | ||
), | ||
); | ||
expect(fetchMock.lastUrl()).toContain( | ||
'/mail-domains/example-slug/accesses/1-1-1-1-1/', | ||
); | ||
}); | ||
|
||
it('calls onError when the API fails', async () => { | ||
fetchMock.deleteOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', { | ||
status: 500, | ||
body: { cause: ['Internal server error'] }, | ||
}); | ||
|
||
const onError = jest.fn(); | ||
|
||
const { result } = renderHook( | ||
() => useDeleteMailDomainAccess({ onError }), | ||
{ | ||
wrapper: AppWrapper, | ||
}, | ||
); | ||
|
||
result.current.mutate({ | ||
slug: 'example-slug', | ||
accessId: '1-1-1-1-1', | ||
}); | ||
|
||
await waitFor(() => expect(fetchMock.calls()).toHaveLength(1)); | ||
await waitFor(() => expect(onError).toHaveBeenCalled()); | ||
}); | ||
}); |
133 changes: 133 additions & 0 deletions
133
.../src/features/mail-domains/access-management/api/__tests__/useMailDomainAccesses.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { renderHook, waitFor } from '@testing-library/react'; | ||
import fetchMock from 'fetch-mock'; | ||
|
||
import { APIError } from '@/api'; | ||
import { AppWrapper } from '@/tests/utils'; | ||
|
||
import { Role } from '../../../domains'; | ||
import { Access } from '../../types'; | ||
import { | ||
getMailDomainAccesses, | ||
useMailDomainAccesses, | ||
} from '../useMailDomainAccesses'; | ||
|
||
const mockAccess: Access = { | ||
id: '1-1-1-1-1', | ||
role: Role.ADMIN, | ||
user: { | ||
id: '2-1-1-1-1', | ||
name: 'username1', | ||
email: 'user1@test.com', | ||
}, | ||
can_set_role_to: [Role.VIEWER, Role.ADMIN], | ||
}; | ||
|
||
describe('getMailDomainAccesses', () => { | ||
afterEach(() => { | ||
fetchMock.restore(); | ||
}); | ||
|
||
it('fetches the list of accesses successfully', async () => { | ||
const mockResponse = { | ||
count: 2, | ||
results: [ | ||
mockAccess, | ||
{ | ||
id: '2', | ||
role: Role.VIEWER, | ||
user: { id: '12', name: 'username2', email: 'user2@test.com' }, | ||
can_set_role_to: [Role.VIEWER], | ||
}, | ||
], | ||
}; | ||
|
||
fetchMock.getOnce('end:/mail-domains/example-slug/accesses/?page=1', { | ||
status: 200, | ||
body: mockResponse, | ||
}); | ||
|
||
const result = await getMailDomainAccesses({ | ||
page: 1, | ||
slug: 'example-slug', | ||
}); | ||
|
||
expect(result).toEqual(mockResponse); | ||
expect(fetchMock.calls()).toHaveLength(1); | ||
expect(fetchMock.lastUrl()).toContain( | ||
'/mail-domains/example-slug/accesses/?page=1', | ||
); | ||
}); | ||
|
||
it('throws an error when the API call fails', async () => { | ||
fetchMock.getOnce('end:/mail-domains/example-slug/accesses/?page=1', { | ||
status: 500, | ||
body: { cause: ['Internal server error'] }, | ||
}); | ||
|
||
await expect( | ||
getMailDomainAccesses({ page: 1, slug: 'example-slug' }), | ||
).rejects.toThrow(APIError); | ||
expect(fetchMock.calls()).toHaveLength(1); | ||
}); | ||
}); | ||
|
||
describe('useMailDomainAccesses', () => { | ||
afterEach(() => { | ||
fetchMock.restore(); | ||
}); | ||
|
||
it('fetches and returns the accesses data using the hook', async () => { | ||
const mockResponse = { | ||
count: 2, | ||
results: [ | ||
mockAccess, | ||
{ | ||
id: '2', | ||
role: Role.VIEWER, | ||
user: { id: '12', name: 'username2', email: 'user2@test.com' }, | ||
can_set_role_to: [Role.VIEWER], | ||
}, | ||
], | ||
}; | ||
|
||
fetchMock.getOnce('end:/mail-domains/example-slug/accesses/?page=1', { | ||
status: 200, | ||
body: mockResponse, | ||
}); | ||
|
||
const { result } = renderHook( | ||
() => useMailDomainAccesses({ page: 1, slug: 'example-slug' }), | ||
{ | ||
wrapper: AppWrapper, | ||
}, | ||
); | ||
|
||
await waitFor(() => result.current.isSuccess); | ||
|
||
await waitFor(() => expect(result.current.data).toEqual(mockResponse)); | ||
expect(fetchMock.calls()).toHaveLength(1); | ||
expect(fetchMock.lastUrl()).toContain( | ||
'/mail-domains/example-slug/accesses/?page=1', | ||
); | ||
}); | ||
|
||
it('handles an API error properly with the hook', async () => { | ||
fetchMock.getOnce('end:/mail-domains/example-slug/accesses/?page=1', { | ||
status: 500, | ||
body: { cause: ['Internal server error'] }, | ||
}); | ||
|
||
const { result } = renderHook( | ||
() => useMailDomainAccesses({ page: 1, slug: 'example-slug' }), | ||
{ | ||
wrapper: AppWrapper, | ||
}, | ||
); | ||
|
||
await waitFor(() => result.current.isError); | ||
|
||
await waitFor(() => expect(result.current.error).toBeInstanceOf(APIError)); | ||
expect(result.current.error?.message).toBe('Failed to get the accesses'); | ||
expect(fetchMock.calls()).toHaveLength(1); | ||
}); | ||
}); |
Oops, something went wrong.