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

✨(frontend) add mail domain access management #413

Merged
merged 2 commits into from
Oct 2, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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
Loading