Skip to content

Commit

Permalink
✨(frontend) add mail domain access management
Browse files Browse the repository at this point in the history
- 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
daproclaima authored and AntoLC committed Oct 2, 2024
1 parent cffce3a commit a14866e
Show file tree
Hide file tree
Showing 30 changed files with 2,593 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to
- ✨(backend) domain accesses create API #428
- 🥅(frontend) catch new errors on mailbox creation #392
- ✨(api) domain accesses delete API #433
- ✨(frontend) add mail domain access management #413

### Fixed

Expand Down
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');
});
});
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());
});
});
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);
});
});
Loading

0 comments on commit a14866e

Please sign in to comment.