Skip to content

Commit

Permalink
✨(frontend) add mail domain access management
Browse files Browse the repository at this point in the history
- move files: separate mail domains feature
into domains, mailboxe and access management
sub features.
- pages/mail-domains/[slug].tsx becomes
pages/mail-domains/[slug]/index.tsx to add
a pages/mail-domains/[slug]/accesses.tsx to
manage accesses.
- access management view is ready to use
get, patch and delete requests once backend
is ready.
- update translations and component tests.
- reduce gap between mail domains feature
logo and mail domain name in top banner
  • Loading branch information
daproclaima committed Sep 19, 2024
1 parent 232ea97 commit 631e63c
Show file tree
Hide file tree
Showing 65 changed files with 2,781 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to

- ✨(domains) add endpoint to list and retrieve domain accesses @sdemagny #404
- 🍱(dev) embark dimail-api as container by @mjeammet #366
- ✨(frontend) add mail domain access management #413

## [1.1.0] - 2024-09-10

Expand Down
2 changes: 1 addition & 1 deletion secrets
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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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 {
updateMailDomainAccess,
useUpdateMailDomainAccess,
} from '../useUpdateMailDomainAccess';

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('updateMailDomainAccess', () => {
afterEach(() => {
fetchMock.restore();
});

it('updates the access role successfully', async () => {
const mockResponse = {
...mockAccess,
role: Role.VIEWER,
};

fetchMock.patchOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', {
status: 200,
body: mockResponse,
});

const result = await updateMailDomainAccess({
slug: 'example-slug',
accessId: '1-1-1-1-1',
role: Role.VIEWER,
});

expect(result).toEqual(mockResponse);
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.patchOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', {
status: 500,
body: { cause: ['Internal server error'] },
});

await expect(
updateMailDomainAccess({
slug: 'example-slug',
accessId: '1-1-1-1-1',
role: Role.VIEWER,
}),
).rejects.toThrow(APIError);
expect(fetchMock.calls()).toHaveLength(1);
});
});

describe('useUpdateMailDomainAccess', () => {
afterEach(() => {
fetchMock.restore();
});

it('updates the role and calls onSuccess callback', async () => {
const mockResponse = {
...mockAccess,
role: Role.VIEWER,
};

fetchMock.patchOnce('end:/mail-domains/example-slug/accesses/1-1-1-1-1/', {
status: 200,
body: mockResponse,
});

const onSuccess = jest.fn();

const { result } = renderHook(
() => useUpdateMailDomainAccess({ onSuccess }),
{
wrapper: AppWrapper,
},
);

result.current.mutate({
slug: 'example-slug',
accessId: '1-1-1-1-1',
role: Role.VIEWER,
});

await waitFor(() => expect(fetchMock.calls()).toHaveLength(1));
await waitFor(() =>
expect(onSuccess).toHaveBeenCalledWith(
mockResponse, // data
{ slug: 'example-slug', accessId: '1-1-1-1-1', role: Role.VIEWER }, // variables
undefined, // context
),
);
expect(fetchMock.lastUrl()).toContain(
'/mail-domains/example-slug/accesses/1-1-1-1-1/',
);
});

it('calls onError when the API fails', async () => {
fetchMock.patchOnce('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(
() => useUpdateMailDomainAccess({ onError }),
{
wrapper: AppWrapper,
},
);

result.current.mutate({
slug: 'example-slug',
accessId: '1-1-1-1-1',
role: Role.VIEWER,
});

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,3 @@
export * from './useMailDomainAccesses';
export * from './useUpdateMailDomainAccess';
export * from './useDeleteMailDomainAccess';
Loading

0 comments on commit 631e63c

Please sign in to comment.