FIX - ajustement delete image et tests

This commit is contained in:
Eddi3_As 2025-03-13 18:02:26 -04:00
parent f4bcfd27b9
commit b7ec35c0c9
7 changed files with 269 additions and 105 deletions

View file

@ -32,7 +32,6 @@ describe("ImageDialog Component", () => {
render( render(
<ImageDialog <ImageDialog
galleryOpen={true} galleryOpen={true}
admin={false}
setDialogOpen={setDialogOpenMock} setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock} setImageLinks={setImageLinksMock}
/> />
@ -50,7 +49,6 @@ describe("ImageDialog Component", () => {
render( render(
<ImageDialog <ImageDialog
galleryOpen={true} galleryOpen={true}
admin={false}
setDialogOpen={setDialogOpenMock} setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock} setImageLinks={setImageLinksMock}
/> />
@ -68,7 +66,6 @@ describe("ImageDialog Component", () => {
render( render(
<ImageDialog <ImageDialog
galleryOpen={true} galleryOpen={true}
admin={false}
setDialogOpen={setDialogOpenMock} setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock} setImageLinks={setImageLinksMock}
/> />
@ -85,32 +82,11 @@ describe("ImageDialog Component", () => {
expect(screen.getByText("Copié!")).toBeInTheDocument(); expect(screen.getByText("Copié!")).toBeInTheDocument();
}); });
test("shows edit field when admin clicks edit button", async () => {
await act(async () => {
render(
<ImageDialog
galleryOpen={true}
admin={true}
setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock}
/>
);
});
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled());
const editButton = screen.getByTestId("edit-button-1");
fireEvent.click(editButton);
expect(screen.getByDisplayValue("image1.jpg")).toBeInTheDocument();
});
test("navigates to next and previous page", async () => { test("navigates to next and previous page", async () => {
await act(async () => { await act(async () => {
render( render(
<ImageDialog <ImageDialog
galleryOpen={true} galleryOpen={true}
admin={false}
setDialogOpen={setDialogOpenMock} setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock} setImageLinks={setImageLinksMock}
/> />
@ -127,4 +103,48 @@ describe("ImageDialog Component", () => {
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3));
}); });
test("deletes an image successfully", async () => {
jest.spyOn(ApiService, "deleteImage").mockResolvedValue(true);
await act(async () => {
render(
<ImageDialog
galleryOpen={true}
setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock}
/>
);
});
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled());
fireEvent.click(screen.getByTestId("delete-button-1"));
await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1"));
expect(screen.queryByTestId("delete-button-1")).not.toBeInTheDocument();
});
test("handles failed delete when image is linked", async () => {
jest.spyOn(ApiService, "deleteImage").mockResolvedValue(false);
await act(async () => {
render(
<ImageDialog
galleryOpen={true}
setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock}
/>
);
});
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled());
fireEvent.click(screen.getByTestId("delete-button-1"));
await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1"));
expect(screen.getByText("Confirmer la suppression")).toBeInTheDocument();
});
}); });

View file

@ -12,31 +12,30 @@ import {
TableRow, TableRow,
IconButton, IconButton,
Paper, Paper,
TextField, Box,
Box CircularProgress
} from "@mui/material"; } from "@mui/material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete";
import { Images } from "../../Types/Images"; import { Images } from "../../Types/Images";
import ApiService from '../../services/ApiService'; import ApiService from '../../services/ApiService';
import { ENV_VARIABLES } from '../../constants'; import { ENV_VARIABLES } from '../../constants';
type Props = { type Props = {
galleryOpen: boolean; galleryOpen: boolean;
admin: boolean;
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>; setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
setImageLinks: React.Dispatch<React.SetStateAction<string[]>>; setImageLinks: React.Dispatch<React.SetStateAction<string[]>>;
} };
const ImageDialog: React.FC<Props> = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => {
const ImageDialog: React.FC<Props> = ({ galleryOpen, setDialogOpen, setImageLinks }) => {
const [copiedId, setCopiedId] = useState<string | null>(null); const [copiedId, setCopiedId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [images, setImages] = useState<Images[]>([]); const [images, setImages] = useState<Images[]>([]);
const [totalImg, setTotalImg] = useState(0); const [totalImg, setTotalImg] = useState(0);
const [imgPage, setImgPage] = useState(1); const [imgPage, setImgPage] = useState(1);
const [imgLimit] = useState(3); const [imgLimit] = useState(3);
const [loading, setLoading] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<{ id: string | null; linked: boolean }>({ id: null, linked: false });
const fetchImages = async (page: number, limit: number) => { const fetchImages = async (page: number, limit: number) => {
const data = await ApiService.getImages(page, limit); const data = await ApiService.getImages(page, limit);
@ -46,37 +45,50 @@ const ImageDialog: React.FC<Props> = ({ galleryOpen, admin, setDialogOpen, setIm
useEffect(() => { useEffect(() => {
fetchImages(imgPage, imgLimit); fetchImages(imgPage, imgLimit);
}, [imgPage]); // Re-fetch images when page changes }, [imgPage]);
const handleEditClick = (id: string) => {
setEditingId(id === editingId ? null : id);
};
const onCopy = (id: string) => { const onCopy = (id: string) => {
const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`;
console.log(escLink);
setCopiedId(id); setCopiedId(id);
setImageLinks(prevLinks => [...prevLinks, escLink]); setImageLinks(prevLinks => [...prevLinks, escLink]);
}; };
const handleDelete = async (id: string) => {
setLoading(true);
const isDeleted = await ApiService.deleteImage(id);
setLoading(false);
if (!isDeleted) {
setDeleteConfirm({ id, linked: true });
} else {
setImages(images.filter(image => image.id !== id));
setDeleteConfirm({ id: null, linked: false });
}
};
const confirmDelete = async () => {
if (deleteConfirm.id) {
setLoading(true);
await ApiService.deleteImage(deleteConfirm.id);
setImages(images.filter(image => image.id !== deleteConfirm.id));
setDeleteConfirm({ id: null, linked: false });
setLoading(false);
}
};
const handleNextPage = () => { const handleNextPage = () => {
if ((imgPage * imgLimit) < totalImg) { if ((imgPage * imgLimit) < totalImg) {
setImgPage(prev => prev + 1); setImgPage(prev => prev + 1);
} }
}; };
const handlePrevPage = () => { const handlePrevPage = () => {
if (imgPage > 1) { if (imgPage > 1) {
setImgPage(prev => prev - 1); setImgPage(prev => prev - 1);
} }
}; };
return ( return (
<Dialog <Dialog open={galleryOpen} onClose={() => setDialogOpen(false)} maxWidth="xl">
open={galleryOpen}
onClose={() => setDialogOpen(false)}
maxWidth="xl" // 'md' stands for medium size
>
<DialogTitle> <DialogTitle>
Images disponibles Images disponibles
<IconButton <IconButton
@ -89,60 +101,67 @@ const ImageDialog: React.FC<Props> = ({ galleryOpen, admin, setDialogOpen, setIm
</IconButton> </IconButton>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<TableContainer component={Paper}> {loading ? (
<Table> <Box display="flex" justifyContent="center" alignItems="center" height={200}>
<TableBody> <CircularProgress />
{images.map((obj: Images) => (
<TableRow key={obj.id}>
<TableCell>
<img
src={`data:${obj.mime_type};base64,${obj.file_content}`}
alt={`Image ${obj.file_name}`}
style={{ width: 350, height: "auto", borderRadius: 8 }}
/>
</TableCell>
<TableCell>
{admin && editingId === obj.id ? (
<TextField
value={obj.file_name}
variant="outlined"
size="small"
style={{ maxWidth: 150 }}
/>
) : (
obj.file_name
)}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{obj.id}
<IconButton onClick={() => onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}>
<ContentCopyIcon fontSize="small" />
</IconButton>
{admin && (
<>
<IconButton onClick={() => handleEditClick(obj.id)} size="small" color="primary" data-testid={`edit-button-${obj.id}`}>
<EditIcon fontSize="small" />
</IconButton>
</>
)}
{copiedId === obj.id && <span style={{ marginLeft: 8, color: "green" }}>Copié!</span>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions style={{ justifyContent: "center", width: "100%" }}>
<Box display="flex" justifyContent="center" width="100%">
<Button onClick={handlePrevPage} disabled={imgPage === 1} color="primary">
Précédent
</Button>
<Button onClick={handleNextPage} disabled={(imgPage * imgLimit) >= totalImg} color="primary">
Suivant
</Button>
</Box> </Box>
</DialogActions> ) : (
<TableContainer component={Paper}>
<Table>
<TableBody>
{images.map((obj: Images) => (
<TableRow key={obj.id}>
<TableCell>
<img
src={`data:${obj.mime_type};base64,${obj.file_content}`}
alt={`Image ${obj.file_name}`}
style={{ width: 350, height: "auto", borderRadius: 8 }}
/>
</TableCell>
<TableCell>{obj.file_name}</TableCell>
<TableCell style={{ minWidth: 150 }}>
{obj.id}
<IconButton onClick={() => onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}>
<ContentCopyIcon fontSize="small" />
</IconButton>
<IconButton onClick={() => handleDelete(obj.id)} size="small" color="secondary" data-testid={`delete-button-${obj.id}`}>
<DeleteIcon fontSize="small" />
</IconButton>
{copiedId === obj.id && <span style={{ marginLeft: 8, color: "green" }}>Copié!</span>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</DialogContent>
{deleteConfirm.linked && (
<Dialog open={Boolean(deleteConfirm.id)} onClose={() => setDeleteConfirm({ id: null, linked: false })}>
<DialogTitle>Confirmer la suppression</DialogTitle>
<DialogContent>
Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ?
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirm({ id: null, linked: false })} color="primary">
Annuler
</Button>
<Button onClick={confirmDelete} color="secondary">
Supprimer
</Button>
</DialogActions>
</Dialog>
)}
<DialogActions style={{ justifyContent: "center", width: "100%" }}>
<Box display="flex" justifyContent="center" width="100%">
<Button onClick={handlePrevPage} disabled={imgPage === 1} color="primary">
Précédent
</Button>
<Button onClick={handleNextPage} disabled={(imgPage * imgLimit) >= totalImg} color="primary">
Suivant
</Button>
</Box>
</DialogActions>
</Dialog> </Dialog>
); );
}; };

View file

@ -1247,6 +1247,36 @@ public async login(email: string, password: string): Promise<any> {
} }
} }
public async deleteImage(imgId: string): Promise<ApiResponse> {
try {
const url: string = this.constructRequestUrl(`/image/delete`);
const headers = this.constructRequestHeaders();
const uid = this.getUserID();
let params = { uid: uid, imgId: imgId };
const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers });
if (result.status !== 200) {
throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`);
}
const deleted = result.data.delete;
return deleted;
} catch (error) {
console.log("Error details: ", error);
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
}
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
}
}
} }
const apiService = new ApiService(); const apiService = new ApiService();

View file

@ -88,7 +88,8 @@ describe('Images', () => {
insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }),
findOne: jest.fn(), findOne: jest.fn(),
find: jest.fn().mockReturnValue(mockFindCursor), find: jest.fn().mockReturnValue(mockFindCursor),
countDocuments: jest.fn() countDocuments: jest.fn(),
deleteOne: jest.fn(),
}; };
dbConn = { dbConn = {
@ -243,4 +244,62 @@ describe('Images', () => {
expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' });
}); });
}); });
describe('delete', () => {
it('should delete the image when it exists', async () => {
const uid = 'user123';
const imgId = 'img123';
// Simulate the image being found in the collection
mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]);
mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 });
const result = await images.delete(uid, imgId);
expect(db.connect).toHaveBeenCalled();
expect(db.getConnection).toHaveBeenCalled();
expect(mockImagesCollection.find).toHaveBeenCalledWith({
userId: uid,
content: { $regex: new RegExp(`/api/image/get/${imgId}`) },
});
expect(mockImagesCollection.deleteOne).toHaveBeenCalledWith({ _id: imgId });
expect(result).toEqual({ deleted: true });
});
it('should not delete the image when it does not exist', async () => {
const uid = 'user123';
const imgId = 'img123';
// Simulate the image not being found in the collection
mockImagesCollection.find.mockResolvedValue([]);
const result = await images.delete(uid, imgId);
expect(db.connect).toHaveBeenCalled();
expect(mockImagesCollection.find).toHaveBeenCalledWith({
userId: uid,
content: { $regex: new RegExp(`/api/image/get/${imgId}`) },
});
expect(mockImagesCollection.deleteOne).toHaveBeenCalled();
expect(result).toEqual({ deleted: false });
});
it('should return false if the delete operation fails', async () => {
const uid = 'user123';
const imgId = 'img123';
// Simulate the image being found, but the delete operation failing
mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]);
const result = await images.delete(uid, imgId);
expect(db.connect).toHaveBeenCalled();
expect(mockImagesCollection.find).toHaveBeenCalledWith({
userId: uid,
content: { $regex: new RegExp(`/api/image/get/${imgId}`) },
});
expect(result).toEqual({ deleted: false });
});
});
}); });

View file

@ -85,6 +85,23 @@ class ImagesController {
} }
}; };
delete = async (req, res, next) => {
try {
const uid = req.query.uid;
const imgId = req.query.imgId;
if (!uid || !imgId) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const images = await this.images.delete(uid, imgId);
res.setHeader('Content-Type', 'application/json');
return res.status(200).json(images);
} catch (error) {
return next(error);
}
};
} }
module.exports = ImagesController; module.exports = ImagesController;

View file

@ -102,6 +102,24 @@ class Images {
return respObj; return respObj;
} }
async delete(uid, imgId) {
let resp = false;
await this.db.connect()
const conn = this.db.getConnection();
const imgsColl = conn.collection('files');
const rgxImg = new RegExp(`/api/image/get/${imgId}`);
const result = await imgsColl.find({ userId: uid, content: { $regex: rgxImg }});
if(result){
const isDeleted = await imgsColl.deleteOne({ _id: imgId });
if(isDeleted){
resp = true;
}
}
return { deleted: resp };
}
} }
module.exports = Images; module.exports = Images;

View file

@ -14,5 +14,6 @@ router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(im
router.get("/get/:id", asyncHandler(images.get)); router.get("/get/:id", asyncHandler(images.get));
router.get("/getImages", asyncHandler(images.getImages)); router.get("/getImages", asyncHandler(images.getImages));
router.get("/getUserImages", asyncHandler(images.getUserImages)); router.get("/getUserImages", asyncHandler(images.getUserImages));
router.get("/delete", asyncHandler(images.delete));
module.exports = router; module.exports = router;