diff --git a/DataSentinel/src/__tests__/Admin.test.jsx b/DataSentinel/src/__tests__/Admin.test.jsx new file mode 100644 index 0000000..b8865b8 --- /dev/null +++ b/DataSentinel/src/__tests__/Admin.test.jsx @@ -0,0 +1,174 @@ +/* Écran d'administration des référentiels. + + Avant correction, les boutons Modifier / Désactiver n'appelaient rien + (console.log) et « Enregistrer » se contentait de fermer la modale. Ces + tests vérifient que chaque action atteint réellement l'API et que les + erreurs métier remontent à l'utilisateur. */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Admin from '../pages/Admin/Admin'; +import * as api from '../services/api'; + +const SERVICES = [ + { id_service: 1, nom_service: 'Contrat' }, + { id_service: 2, nom_service: 'Fournisseur' }, +]; +const CATEGORIES = [{ id_categorie: 1, intitule_categorie: 'Tiers-payeurs' }]; +const MONITORINGS = [{ + id_monito: 1, monito_intitule: 'Erreurs tiers payeurs', id_service: 1, + id_categorie: 1, table_source: 'MONITO_TIERS_PAYEURS', bdd_source: 'Sage', +}]; + +function stubLectures() { + vi.spyOn(api, 'getServices').mockResolvedValue(SERVICES); + vi.spyOn(api, 'getCategories').mockResolvedValue(CATEGORIES); + vi.spyOn(api, 'getContacts').mockResolvedValue([]); + vi.spyOn(api, 'getMonitorings').mockResolvedValue(MONITORINGS); +} + +async function ouvrirOnglet(utilisateur, nom) { + await utilisateur.click(screen.getByRole('button', { name: nom })); +} + +describe('Admin — CRUD des référentiels', () => { + beforeEach(() => { + vi.restoreAllMocks(); + stubLectures(); + vi.spyOn(window, 'confirm').mockReturnValue(true); + }); + + it('crée un service via l’API et recharge la liste', async () => { + const creation = vi.spyOn(api, 'createService').mockResolvedValue({ id_service: 3 }); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + await utilisateur.click(screen.getByRole('button', { name: /Ajouter/ })); + + await utilisateur.type(screen.getByLabelText('Nom du service'), 'Comptabilité'); + await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' })); + + await waitFor(() => { + expect(creation).toHaveBeenCalledWith({ nom_service: 'Comptabilité' }); + }); + /* Rechargement après écriture : la liste doit être redemandée. */ + expect(api.getServices).toHaveBeenCalledTimes(2); + }); + + it('pré-remplit le formulaire de modification avec la ligne choisie', async () => { + vi.spyOn(api, 'updateService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Fournisseur').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ })); + + expect(screen.getByLabelText('Nom du service')).toHaveValue('Fournisseur'); + }); + + it('envoie l’identifiant de la ligne lors d’une modification', async () => { + const modification = vi.spyOn(api, 'updateService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Fournisseur').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ })); + await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' })); + + await waitFor(() => { + expect(modification).toHaveBeenCalledWith(2, { nom_service: 'Fournisseur' }); + }); + }); + + it('demande confirmation avant de supprimer, et n’appelle rien si on refuse', async () => { + window.confirm.mockReturnValue(false); + const suppression = vi.spyOn(api, 'deleteService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Contrat').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ })); + + expect(window.confirm).toHaveBeenCalled(); + expect(suppression).not.toHaveBeenCalled(); + }); + + it('affiche le refus de l’API quand des enregistrements sont rattachés', async () => { + /* Cas réel : l'API renvoie 409 avec le décompte des rattachements. */ + vi.spyOn(api, 'deleteService').mockRejectedValue( + new Error('Erreur 409: Suppression impossible : 5 monitoring(s) rattachés.'), + ); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Contrat').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ })); + + expect(await screen.findByText(/5 monitoring\(s\) rattachés/)).toBeInTheDocument(); + }); + + it('parle de désactivation, pas de suppression, pour un monitoring', async () => { + /* TABLE_FINAL référence le monitoring : il n'est jamais supprimé. */ + const desactivation = vi.spyOn(api, 'desactiverMonitoring').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + + const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr'); + expect(within(ligne).getByRole('button', { name: /Désactiver/ })).toBeInTheDocument(); + + await utilisateur.click(within(ligne).getByRole('button', { name: /Désactiver/ })); + await waitFor(() => expect(desactivation).toHaveBeenCalledWith(1)); + }); + + it('affiche le libellé du service plutôt que son identifiant brut', async () => { + render(); + await screen.findByText('Administration'); + + const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr'); + /* id_service = 1 doit s'afficher « Contrat ». */ + expect(within(ligne).getByText('Contrat')).toBeInTheDocument(); + }); + + it('convertit en entier les identifiants issus des listes déroulantes', async () => { + /* Un (YYYY-MM-DD). - On lit les composantes en heure locale plutôt que d'utiliser toISOString(), - qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le - 31 décembre de l'année précédente. */ -function formatDateISO(date) { - const mois = String(date.getMonth() + 1).padStart(2, '0'); - const jour = String(date.getDate()).padStart(2, '0'); - return `${date.getFullYear()}-${mois}-${jour}`; -} - -/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */ -function periodeParDefaut() { - const aujourdhui = new Date(); - return { - debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)), - fin: formatDateISO(aujourdhui), - }; -} - /* Récupère le tableau d'historique et la série du graphique pour les filtres donnés. Sans monitoring sélectionné, on prend la courbe globale. Fonction pure : elle ne touche à aucun état, ce qui permet de l'appeler diff --git a/DataSentinel/src/utils/dates.js b/DataSentinel/src/utils/dates.js new file mode 100644 index 0000000..8e9d12f --- /dev/null +++ b/DataSentinel/src/utils/dates.js @@ -0,0 +1,17 @@ +/* Formate une date au format attendu par (YYYY-MM-DD). + On lit les composantes en heure locale plutôt que d'utiliser toISOString(), + qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le + 31 décembre de l'année précédente. */ +export function formatDateISO(date) { + const mois = String(date.getMonth() + 1).padStart(2, '0'); + const jour = String(date.getDate()).padStart(2, '0'); + return `${date.getFullYear()}-${mois}-${jour}`; +} + +/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */ +export function periodeParDefaut(aujourdhui = new Date()) { + return { + debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)), + fin: formatDateISO(aujourdhui), + }; +}