(frontend) display button to re-run fetch domain from dimail

Add the button in the modal which describes actions required
to make the domain work
This commit is contained in:
Sabrina Demagny
2025-02-16 17:57:36 +01:00
parent cdb766b0e0
commit 29d0bbb692
7 changed files with 242 additions and 15 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added
- ✨(domains) allow user to re-run all fetch domain data from dimail
- ✨(domains) display DNS config expected for domain with required actions
- ✨(domains) check status after creation
- ✨(domains) display required actions to do on domain

View File

@@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { VariantType } from '@openfun/cunningham-react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import { MailDomain } from '@/features/mail-domains/domains';
@@ -6,14 +7,6 @@ import { AppWrapper } from '@/tests/utils';
import { MailDomainView } from '../components/MailDomainView';
const toast = jest.fn();
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
useToastProvider: () => ({
toast,
}),
}));
const mockMailDomain: MailDomain = {
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'example.com',
@@ -56,7 +49,17 @@ const mockMailDomain: MailDomain = {
],
};
const toast = jest.fn();
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
useToastProvider: () => ({
toast,
}),
}));
describe('<MailDomainView />', () => {
const apiUrl = `end:/mail-domains/${mockMailDomain.slug}/fetch/`;
beforeEach(() => {
jest.clearAllMocks();
});
@@ -64,6 +67,7 @@ describe('<MailDomainView />', () => {
afterEach(() => {
fetchMock.restore();
});
it('display action required button and open modal with information when domain status is action_required', () => {
render(<MailDomainView mailDomain={mockMailDomain} />, {
wrapper: AppWrapper,
@@ -79,7 +83,7 @@ describe('<MailDomainView />', () => {
expect(screen.getByText('Required actions on domain')).toBeInTheDocument();
expect(
screen.getByText(
/Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr/,
/Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr./i,
),
).toBeInTheDocument();
@@ -92,4 +96,45 @@ describe('<MailDomainView />', () => {
screen.getByText(/webmail.ox.numerique.gouv.fr./i),
).toBeInTheDocument();
});
it('allows re-running domain check when clicking re-run button', async () => {
// Mock the fetch call
fetchMock.postOnce(apiUrl, {
status: 200,
body: mockMailDomain,
});
render(
<MailDomainView
mailDomain={mockMailDomain}
onMailDomainUpdate={jest.fn()}
/>,
{ wrapper: AppWrapper },
);
// Check if action required button is displayed
const actionButton = screen.getByText('Actions required');
expect(actionButton).toBeInTheDocument();
// Click the button and verify modal content
fireEvent.click(actionButton);
// Verify modal title and content
expect(screen.getByText('Required actions on domain')).toBeInTheDocument();
expect(
screen.getByText(
/Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr./,
),
).toBeInTheDocument();
// Find and click re-run button
const reRunButton = screen.getByText('Re-run check');
fireEvent.click(reRunButton);
await waitFor(() => {
expect(fetchMock.called(apiUrl)).toBeTruthy();
});
expect(toast).toHaveBeenCalledWith(
'Domain data fetched successfully',
VariantType.SUCCESS,
);
});
});

View File

@@ -0,0 +1,76 @@
import fetchMock from 'fetch-mock';
import { APIError } from '@/api';
import { fetchMailDomain } from '@/features/mail-domains/domains/api/useFetchMailDomain';
import { MailDomain } from '@/features/mail-domains/domains/types';
const mockMailDomain: MailDomain = {
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'example.com',
status: 'enabled',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
slug: 'example-com',
support_email: 'support@example.com',
abilities: {
delete: false,
manage_accesses: true,
get: true,
patch: true,
put: true,
post: true,
},
action_required_details: {
mx: 'Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr.',
},
expected_config: [
{ target: '', type: 'mx', value: 'mx.ox.numerique.gouv.fr.' },
{
target: 'dimail._domainkey',
type: 'txt',
value: 'v=DKIM1; h=sha256; k=rsa; p=X...X',
},
{ target: 'imap', type: 'cname', value: 'imap.ox.numerique.gouv.fr.' },
{ target: 'smtp', type: 'cname', value: 'smtp.ox.numerique.gouv.fr.' },
{
target: '',
type: 'txt',
value: 'v=spf1 include:_spf.ox.numerique.gouv.fr -all',
},
{
target: 'webmail',
type: 'cname',
value: 'webmail.ox.numerique.gouv.fr.',
},
],
};
describe('fetchMailDomain', () => {
afterEach(() => {
fetchMock.restore();
});
it('fetch the domain successfully', async () => {
fetchMock.postOnce('end:/mail-domains/example-slug/fetch/', {
status: 200,
body: mockMailDomain,
});
const result = await fetchMailDomain('example-slug');
expect(result).toEqual(mockMailDomain);
expect(fetchMock.calls()).toHaveLength(1);
expect(fetchMock.lastUrl()).toContain('/mail-domains/example-slug/fetch/');
});
it('throw an error when the domain is not found', async () => {
fetchMock.postOnce('end:/mail-domains/example-slug/fetch/', {
status: 404,
body: { cause: ['Domain not found'] },
});
await expect(fetchMailDomain('example-slug')).rejects.toThrow(APIError);
expect(fetchMock.calls()).toHaveLength(1);
expect(fetchMock.lastUrl()).toContain('/mail-domains/example-slug/fetch/');
});
});

View File

@@ -0,0 +1,38 @@
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { MailDomain } from '../types';
export const fetchMailDomain = async (slug: string): Promise<MailDomain> => {
const response = await fetchAPI(`mail-domains/${slug}/fetch/`, {
method: 'POST',
});
if (!response.ok) {
throw new APIError(
'Failed to fetch domain from Dimail',
await errorCauses(response),
);
}
return response.json() as Promise<MailDomain>;
};
export const useFetchFromDimail = ({
onSuccess,
onError,
}: {
onSuccess: (data: MailDomain) => void;
onError: (error: APIError) => void;
}) => {
return useMutation<MailDomain, APIError, string>({
mutationFn: fetchMailDomain,
onSuccess: (data) => {
onSuccess(data);
},
onError: (error) => {
onError(error);
},
});
};

View File

@@ -1,4 +1,5 @@
import {
Button,
Modal,
ModalSize,
VariantType,
@@ -15,10 +16,14 @@ import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.sv
import { MailDomain, Role } from '@/features/mail-domains/domains';
import { MailDomainsContent } from '@/features/mail-domains/mailboxes';
import { useFetchFromDimail } from '../api/useFetchMailDomain';
type Props = {
mailDomain: MailDomain;
onMailDomainUpdate?: (updatedDomain: MailDomain) => void;
};
export const MailDomainView = ({ mailDomain }: Props) => {
export const MailDomainView = ({ mailDomain, onMailDomainUpdate }: Props) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const [showModal, setShowModal] = React.useState(false);
@@ -57,6 +62,19 @@ export const MailDomainView = ({ mailDomain }: Props) => {
toast(t('copy done'), VariantType.SUCCESS);
};
const { mutate: fetchMailDomain } = useFetchFromDimail({
onSuccess: (data: MailDomain) => {
console.info('fetchMailDomain success', data);
setShowModal(false);
toast(t('Domain data fetched successfully'), VariantType.SUCCESS);
onMailDomainUpdate?.(data);
},
onError: () => {
console.error('fetchMailDomain error');
toast(t('Failed to fetch domain data'), VariantType.ERROR);
},
});
return (
<>
{showModal && (
@@ -135,6 +153,17 @@ export const MailDomainView = ({ mailDomain }: Props) => {
</pre>
</Box>
)}
<pre>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Button
onClick={() => {
void fetchMailDomain(mailDomain.slug);
}}
>
{t('Re-run check')}
</Button>
</div>
</pre>
</Modal>
)}
<Box $padding="big">
@@ -155,6 +184,25 @@ export const MailDomainView = ({ mailDomain }: Props) => {
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
{/* TODO: remove when pending status will be removed */}
{mailDomain?.status === 'pending' && (
<button
onClick={handleShowModal}
style={{
padding: '5px 10px',
marginLeft: '10px',
backgroundColor: '#cccccc',
border: 'none',
color: 'white',
cursor: 'pointer',
fontWeight: '500',
borderRadius: '5px',
}}
data-modal="mail-domain-status"
>
{t('Pending')}
</button>
)}
{mailDomain?.status === 'action_required' && (
<button
onClick={handleShowModal}

View File

@@ -74,6 +74,7 @@
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
"Disable": "Désactiver",
"Disable mailbox": "Désactiver la boîte mail",
"Domain data fetched successfully": "Les données de domaine ont été récupérées avec succès",
"Domain name": "Nom de domaine",
"E-mail:": "E-mail:",
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
@@ -86,6 +87,7 @@
"Example: saint-laurent.fr": "Exemple : saint-laurent.fr",
"Failed to add {{name}} in the team": "Impossible d'ajouter {{name}} au groupe",
"Failed to create the invitation for {{email}}": "Impossible de créer l'invitation pour {{email}}",
"Failed to fetch domain data": "Impossible de récupérer les données du domaine",
"Failed to update mailbox status": "Impossible de mettre à jour le statut de la boîte mail",
"Filter member list": "Filtrer la liste des membres",
"Find a member to add to the team": "Trouver un membre à ajouter au groupe",
@@ -145,6 +147,7 @@
"Open the teams panel": "Ouvrir le panneau des groupes",
"Ouch!": "Aïe !",
"Owner": "Propriétaire",
"Pending": "En attente",
"Personal data and cookies": "Données personnelles et cookies",
"Please enter a valid email address": "Merci de saisir une adresse e-mail valide",
"Please enter a valid email address.\nE.g. : jean.dupont@mail.fr": "Veuillez entrer une adresse e-mail valide.\nEx. : jean.dupont@mail.fr",
@@ -153,6 +156,7 @@
"Publication Director": "Directeur de la publication",
"Publisher": "Éditeur",
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
"Re-run check": "Ré-exécuter la vérification",
"Remedy": "Voie de recours",
"Remove from domain": "Retirer du domaine",
"Remove from group": "Retirer du groupe",

View File

@@ -1,11 +1,12 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
import React, { ReactElement } from 'react';
import React, { ReactElement, useState } from 'react';
import { Box } from '@/components';
import { TextErrors } from '@/components/TextErrors';
import {
MailDomain,
MailDomainsLayout,
useMailDomain,
} from '@/features/mail-domains/domains';
@@ -14,13 +15,15 @@ import { NextPageWithLayout } from '@/types/next';
const MailboxesPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentMailDomain, setCurrentMailDomain] = useState<MailDomain | null>(
null,
);
if (router?.query?.slug && typeof router.query.slug !== 'string') {
throw new Error('Invalid mail domain slug');
}
const { slug } = router.query;
const navigate = useNavigate();
const {
@@ -30,6 +33,13 @@ const MailboxesPage: NextPageWithLayout = () => {
isLoading,
} = useMailDomain({ slug: String(slug) });
// Update currentMailDomain when mailDomain changes
React.useEffect(() => {
if (mailDomain) {
setCurrentMailDomain(mailDomain);
}
}, [mailDomain]);
if (error?.status === 404) {
navigate.replace(`/404`);
return null;
@@ -47,11 +57,16 @@ const MailboxesPage: NextPageWithLayout = () => {
);
}
if (!mailDomain) {
if (!currentMailDomain) {
return null;
}
return <MailDomainView mailDomain={mailDomain} />;
return (
<MailDomainView
mailDomain={currentMailDomain}
onMailDomainUpdate={setCurrentMailDomain}
/>
);
};
MailboxesPage.getLayout = function getLayout(page: ReactElement) {