From 7e8864c6b9dd7338e1f60ffda35f6bb8af27593d Mon Sep 17 00:00:00 2001 From: jaimeeg5 Date: Mon, 26 Oct 2026 20:03:57 +0100 Subject: [PATCH 1/3] feat: add component deletion in backend and tests --- .../controller/ComponentsRestController.java | 8 +++ .../pcmod/model/Component.java | 3 +- .../pcmod/service/ComponentsService.java | 15 ++++- .../pcmod/e2e/api/ComponentsSystemTests.java | 54 ++++++++++++++++ .../ComponentsIntegrationTests.java | 62 +++++++++++++++++++ .../pcmod/unit/ComponentsUnitTests.java | 33 ++++++++++ 6 files changed, 173 insertions(+), 2 deletions(-) diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/controller/ComponentsRestController.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/controller/ComponentsRestController.java index 047e7d7..3b5dbff 100644 --- a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/controller/ComponentsRestController.java +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/controller/ComponentsRestController.java @@ -1,5 +1,6 @@ package es.codeurjcstudents.pcmod.controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -54,4 +55,11 @@ public ResponseEntity getComponentImage(@PathVariable long id) throws return ResponseEntity.ok().contentType(mediaType).body(imageFile); } + @DeleteMapping("/{id}") + public void deleteComponent(@PathVariable long id) { + + componentService.delete(id); + + } + } diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Component.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Component.java index 193620c..f0abcf5 100644 --- a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Component.java +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/model/Component.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -22,7 +23,7 @@ public class Component { @GeneratedValue(strategy = GenerationType.AUTO) private Long id = null; - @OneToOne + @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true) private Image image; @NotBlank(message = "El nombre del componente es obligatorio") diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ComponentsService.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ComponentsService.java index af13137..b4d531a 100644 --- a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ComponentsService.java +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/ComponentsService.java @@ -4,12 +4,12 @@ import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.server.ResponseStatusException; import org.springframework.stereotype.Service; @@ -58,4 +58,17 @@ public Resource getImageFile(long id) throws SQLException { return null; } + + @PreAuthorize("hasRole('ADMIN')") + public void delete(long id) { + + Optional deletedComponent = componentRepository.findById(id); + + if (deletedComponent.isPresent()) { + + componentRepository.deleteById(id); + + } + + } } diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java index 367a543..bd875dd 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java @@ -88,4 +88,58 @@ public void getComponentImage() throws IOException { assertArrayEquals(expectedImage, actualImage); } + @Test + public void deleteComponentAsNotLoggedIn() { + + given() + .when().delete("components/1") + .then().statusCode(401).contentType(ContentType.JSON) + .body("status", equalTo(401)) + .body("error", equalTo("Unauthorized")); + } + + @Test + public void deleteComponentAsLoggedInNoAdmin() { + String authToken = given() + .header("Content-Type", "application/json") + .body(""" + { + "username": "user@example.com", + "password": "userpass" + } + """) + .when().post("auth/login") + .then().statusCode(200).contentType(ContentType.JSON) + .extract().cookie("AuthToken"); + + given() + .header("Content-Type", "application/json") + .cookie("AuthToken", authToken) + .when().delete("components/1") + .then().statusCode(403).contentType(ContentType.JSON) + .body("status", equalTo(403)) + .body("error", equalTo("Forbidden")); + } + + @Test + public void deleteComponentAsAdmin() { + String authToken = given() + .header("Content-Type", "application/json") + .body(""" + { + "username": "admin@example.com", + "password": "adminpass" + } + """) + .when().post("auth/login") + .then().statusCode(200).contentType(ContentType.JSON) + .extract().cookie("AuthToken"); + + given() + .header("Content-Type", "application/json") + .cookie("AuthToken", authToken) + .when().delete("components/1") + .then().statusCode(200); + } + } diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ComponentsIntegrationTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ComponentsIntegrationTests.java index 7ae011d..c6f319b 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ComponentsIntegrationTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/integration/ComponentsIntegrationTests.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.sql.SQLException; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -18,6 +19,13 @@ import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.web.server.ResponseStatusException; @@ -27,7 +35,9 @@ import es.codeurjcstudents.pcmod.enums.ComponentType; import es.codeurjcstudents.pcmod.model.Component; +import es.codeurjcstudents.pcmod.model.User; import es.codeurjcstudents.pcmod.repository.ComponentsRepository; +import es.codeurjcstudents.pcmod.repository.UsersRepository; import es.codeurjcstudents.pcmod.service.ComponentsService; import es.codeurjcstudents.pcmod.service.ImageService; @@ -61,6 +71,11 @@ static void configureProperties(DynamicPropertyRegistry registry) { @Autowired private ImageService imageService; + @Autowired + private UsersRepository usersRepository; + + private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + @BeforeEach void setUp() { componentsRepository.deleteAll(); @@ -77,6 +92,14 @@ void setUp() { "DisplayPort 2.1 y HDMI 2.1b hasta 8K\r\n" + // "Tensor Cores 5ª gen y Reflex 2 para IA avanzada", ComponentType.GPU, "Gigabyte", BigDecimal.valueOf(359.85), 2)); + + SecurityContextHolder.clearContext(); + usersRepository.deleteAll(); + usersRepository.save(new User("user1", "test", "userTest1", "c/testaddress", "testuser1@example.com", + passwordEncoder.encode("pass"), "REGISTERED_USER")); + usersRepository.save(new User("admin", "test", "adminTest1", "c/testaddress", "testadmin1@example.com", + passwordEncoder.encode("adminpass"), "REGISTERED_USER", "ADMIN")); + } @Test @@ -129,4 +152,43 @@ public void testGetNullComponentImage() throws SQLException { assertEquals(null, actualImage); } + @Test + void deleteComponentAsNotLoggedIn() { + + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> componentsService.delete(1L)); + + } + + @Test + void deleteComponentAsLoggedInNoAdmin() { + User user = usersRepository.findByEmail("testuser1@example.com").orElseThrow(); + Component component = componentsRepository.findAll().get(0); + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + org.springframework.security.core.userdetails.User.withUsername(user.getId().toString()) + .password("").authorities("ROLE_REGISTERED_USER").build(), + null, List.of(new SimpleGrantedAuthority("ROLE_REGISTERED_USER")))); + + assertThrows(AuthorizationDeniedException.class, () -> componentsService.delete(component.getId())); + assertEquals(2, componentsRepository.count()); + + } + + @Test + void deleteComponentAsAdmin() { + User admin = usersRepository.findByEmail("testadmin1@example.com").orElseThrow(); + Component component = componentsRepository.findAll().get(0); + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + org.springframework.security.core.userdetails.User.withUsername(admin.getId().toString()) + .password("").authorities("ROLE_ADMIN").build(), + null, java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))); + + assertDoesNotThrow(() -> componentsService.delete(component.getId())); + assertEquals(1, componentsRepository.count()); + + } + } \ No newline at end of file diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ComponentsUnitTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ComponentsUnitTests.java index 1ecf415..05357c9 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ComponentsUnitTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/unit/ComponentsUnitTests.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -111,4 +112,36 @@ public void testGetNullComponentImage() throws SQLException { verify(componentsRepository).findImageById(1L); } + @Test + public void testDeleteComponent() { + + ComponentsRepository componentsRepository = mock(ComponentsRepository.class); + ComponentMapper componentMapper = mock(ComponentMapper.class); + ComponentsService componentsService = new ComponentsService(componentsRepository, componentMapper); + + Component component = new Component("Intel i5 12400F", "Procesador de 6 núcleos y 12 hilos", ComponentType.CPU, "Intel", BigDecimal.valueOf(153.97), 14); + + when(componentsRepository.findById(1L)).thenReturn(Optional.of(component)); + + componentsService.delete(1L); + + verify(componentsRepository).deleteById(1L); + + } + + @Test + public void testDeleteNonExistentComponent() { + + ComponentsRepository componentsRepository = mock(ComponentsRepository.class); + ComponentMapper componentMapper = mock(ComponentMapper.class); + ComponentsService componentsService = new ComponentsService(componentsRepository, componentMapper); + + when(componentsRepository.findById(1L)).thenReturn(Optional.empty()); + + componentsService.delete(1L); + + verify(componentsRepository, never()).deleteById(1L); + + } + } From d1ddd9bbdf2e5064d6bd71792ec764cf9fac3c11 Mon Sep 17 00:00:00 2001 From: jaimeeg5 Date: Tue, 27 Oct 2026 12:15:25 +0100 Subject: [PATCH 2/3] feat: add component deletion in frontend and tests --- .../pcmod/e2e/ui/ComponentsSystemTests.java | 113 ++++++++++++++++ frontend/app/routes/detail-component.tsx | 45 +++++- frontend/app/services/components-service.ts | 13 ++ .../ComponentDetailPage.integration.test.tsx | 128 ++++++++++++++++-- .../tests/unit/ComponentDetailPage.test.tsx | 117 +++++++++++++++- 5 files changed, 402 insertions(+), 14 deletions(-) diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java index 96e29cf..7a3db19 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.math.BigDecimal; import java.time.Duration; import java.util.Map; @@ -17,13 +18,23 @@ import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import es.codeurjcstudents.pcmod.enums.ComponentType; +import es.codeurjcstudents.pcmod.model.Component; +import es.codeurjcstudents.pcmod.repository.ComponentsRepository; @Tag("client-system") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ComponentsSystemTests { private WebDriver driver; private WebDriverWait wait; + @Autowired + private ComponentsRepository componentsRepository; + @BeforeEach public void setupTest() { ChromeOptions options = new ChromeOptions(); @@ -232,4 +243,106 @@ public void componentDetailOptionsAsAdminTest() { assertThat(driver.findElements(By.id("delete-component-button"))).isNotEmpty(); } + + @Test + public void deleteComponentTest() { + + Component component = new Component("Intel Core i7-12700K", + "12 núcleos y 20 hilos para un rendimiento máximo\r\n" + // + "Cache L3 de 25 MB\r\n" + // + "Compatible con memorias DDR4 y DDR5\r\n" + // + "Frecuencia base 3.6 GHz y turbo hasta 5.0 GHz", + ComponentType.CPU, "Intel", BigDecimal.valueOf(450.00), 5); + component = componentsRepository.save(component); + + driver.get("http://localhost:5173/login"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email"))); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("password"))); + driver.findElement(By.id("email")).sendKeys("admin@example.com"); + driver.findElement(By.id("password")).sendKeys("adminpass"); + + scrollToAndClick(By.name("login-button")); + + wait.until(ExpectedConditions.urlToBe("http://localhost:5173/")); + + driver.get("http://localhost:5173/components/" + component.getId()); + + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("delete-component-button"))); + scrollToAndClick(By.id("delete-component-button")); + WebElement deleteModal = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".modal.show"))); + assertThat(deleteModal.findElement(By.className("modal-title")).getText()) + .isEqualTo("¿Está seguro de que desea borrar este componente?"); + assertThat(deleteModal.findElement(By.className("modal-body")).getText()) + .contains("Toda su información se perderá y no se podrá recuperar.") + .contains("Esta acción no se puede deshacer."); + scrollToAndClick(By.cssSelector(".modal-footer .pcmod-danger-button")); + + driver.get("http://localhost:5173/components?page=2"); + assertThat(driver.getPageSource()).doesNotContain(component.getName()); + + } + + @Test + public void cancelDeleteComponentTest() { + + driver.get("http://localhost:5173/login"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email"))); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("password"))); + driver.findElement(By.id("email")).sendKeys("admin@example.com"); + driver.findElement(By.id("password")).sendKeys("adminpass"); + + scrollToAndClick(By.name("login-button")); + + wait.until(ExpectedConditions.urlToBe("http://localhost:5173/")); + + driver.get("http://localhost:5173/components/1"); + + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("delete-component-button"))); + scrollToAndClick(By.id("delete-component-button")); + WebElement deleteModal = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".modal.show"))); + assertThat(deleteModal.findElement(By.className("modal-title")).getText()) + .isEqualTo("¿Está seguro de que desea borrar este componente?"); + assertThat(deleteModal.findElement(By.className("modal-body")).getText()) + .contains("Toda su información se perderá y no se podrá recuperar.") + .contains("Esta acción no se puede deshacer."); + scrollToAndClick(By.cssSelector(".modal-footer .btn-secondary")); + + driver.get("http://localhost:5173/components"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("component-1"))); + assertThat(driver.findElement(By.id("component-1")).getText()) + .isEqualTo("AMD Ryzen 7 7800X3D"); + + } + + @Test + public void cancelDeleteComponentClosingModalTest() { + + driver.get("http://localhost:5173/login"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email"))); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("password"))); + driver.findElement(By.id("email")).sendKeys("admin@example.com"); + driver.findElement(By.id("password")).sendKeys("adminpass"); + + scrollToAndClick(By.name("login-button")); + + wait.until(ExpectedConditions.urlToBe("http://localhost:5173/")); + + driver.get("http://localhost:5173/components/1"); + + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("delete-component-button"))); + scrollToAndClick(By.id("delete-component-button")); + WebElement deleteModal = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".modal.show"))); + assertThat(deleteModal.findElement(By.className("modal-title")).getText()) + .isEqualTo("¿Está seguro de que desea borrar este componente?"); + assertThat(deleteModal.findElement(By.className("modal-body")).getText()) + .contains("Toda su información se perderá y no se podrá recuperar.") + .contains("Esta acción no se puede deshacer."); + scrollToAndClick(By.cssSelector(".modal-header .btn-close")); + + driver.get("http://localhost:5173/components"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("component-1"))); + assertThat(driver.findElement(By.id("component-1")).getText()) + .isEqualTo("AMD Ryzen 7 7800X3D"); + + } } \ No newline at end of file diff --git a/frontend/app/routes/detail-component.tsx b/frontend/app/routes/detail-component.tsx index 7b064a2..6b1a922 100644 --- a/frontend/app/routes/detail-component.tsx +++ b/frontend/app/routes/detail-component.tsx @@ -1,9 +1,11 @@ -import { Button, Col, Image } from "react-bootstrap"; -import { CartPlus, Image as ImageIcon, PencilSquare, Trash, XOctagon } from "react-bootstrap-icons"; +import { Button, Col, Image, Modal } from "react-bootstrap"; +import { ArrowLeft, CartPlus, Image as ImageIcon, PencilSquare, Trash, XOctagon } from "react-bootstrap-icons"; import type { Route } from "./+types/detail-component"; import type ComponentDTO from "~/dtos/ComponentDTO"; -import { getComponent } from "~/services/components-service"; +import { deleteComponent, getComponent } from "~/services/components-service"; import { useUserStore } from "~/stores/user-store"; +import { useState } from "react"; +import { useNavigate } from "react-router"; const API_URL = "/api/v1/components"; @@ -16,8 +18,21 @@ export default function ComponentDetail({ loaderData }: Route.ComponentProps) { const component = loaderData; const price = component.price.toFixed(2); let { user } = useUserStore(); + const navigate = useNavigate(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + async function handleDeleteComponent() { + try { + await deleteComponent(component.id); + await navigate("/components"); + } catch (error) { + console.error(error); + } + } return ( + <>
@@ -53,7 +68,7 @@ export default function ComponentDetail({ loaderData }: Route.ComponentProps) { - @@ -71,6 +86,28 @@ export default function ComponentDetail({ loaderData }: Route.ComponentProps) {
+ + setIsDeleteModalOpen(false)}> + + ¿Está seguro de que desea borrar este componente? + + +

+ Toda su información se perderá y no se podrá recuperar. Esta acción no se puede deshacer. +

+
+ + + + +
+ ); } \ No newline at end of file diff --git a/frontend/app/services/components-service.ts b/frontend/app/services/components-service.ts index 8c279e7..1a56c3e 100644 --- a/frontend/app/services/components-service.ts +++ b/frontend/app/services/components-service.ts @@ -70,4 +70,17 @@ export async function getComponent(id: number): Promise { } return await res.json(); +} + +export async function deleteComponent(id: number): Promise { + + const url = new URL(`${API_URL}/${id}`, getBaseUrl()); + const response = await fetch(url.toString(), { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Error deleting component"); + } + } \ No newline at end of file diff --git a/frontend/tests/integration/ComponentDetailPage.integration.test.tsx b/frontend/tests/integration/ComponentDetailPage.integration.test.tsx index 76c729e..af824fe 100644 --- a/frontend/tests/integration/ComponentDetailPage.integration.test.tsx +++ b/frontend/tests/integration/ComponentDetailPage.integration.test.tsx @@ -7,6 +7,8 @@ import "@testing-library/jest-dom"; import { CookieJar } from "tough-cookie"; import ComponentDetail, { clientLoader } from "~/routes/detail-component"; import Login from "~/routes/login"; +import Components from "~/routes/components"; +import { getComponents } from "~/services/components-service"; describe("ComponentDetailPageIntegration", () => { @@ -44,9 +46,11 @@ describe("ComponentDetailPageIntegration", () => { const componentData = await clientLoader({ params: { id: "1" } } as Parameters[0]); render( - , + + + ); await waitFor(() => { @@ -58,9 +62,11 @@ describe("ComponentDetailPageIntegration", () => { const componentData = await clientLoader({ params: { id: "3" } } as Parameters[0]); render( - , + + + ); await waitFor(() => { @@ -72,9 +78,11 @@ describe("ComponentDetailPageIntegration", () => { const componentData = await clientLoader({ params: { id: "1" } } as Parameters[0]); render( - + + + ); await waitFor(() => { @@ -126,4 +134,106 @@ describe("ComponentDetailPageIntegration", () => { }); }); + it("cancel component deletion", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/login", Component: Login }, + { path: "/", Component: () => }, + { path: "/components/:id", Component: ComponentDetail, loader: clientLoader }, + ]); + + render(); + + await user.type(screen.getByLabelText("Correo electrónico:"), "admin@example.com"); + await user.type(screen.getByLabelText("Contraseña:"), "adminpass"); + await user.click(screen.getByRole("button", { name: /Iniciar sesión/i })); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + expect(await screen.findByText("¿Está seguro de que desea borrar este componente?")).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Cancelar/ })).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Eliminar componente/ })).toBeInTheDocument(); + + await user.click(await screen.findByRole("button", { name: /Cancelar/ })); + + await waitFor(() => { + expect(screen.queryByText("AMD Ryzen 7 7800X3D")).toBeInTheDocument(); + }); + + }); + + it("cancel component deletion with the close button", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/login", Component: Login }, + { path: "/", Component: () => }, + { path: "/components/:id", Component: ComponentDetail, loader: clientLoader }, + ]); + + render(); + + await user.type(screen.getByLabelText("Correo electrónico:"), "admin@example.com"); + await user.type(screen.getByLabelText("Contraseña:"), "adminpass"); + await user.click(screen.getByRole("button", { name: /Iniciar sesión/i })); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + expect(await screen.findByText("¿Está seguro de que desea borrar este componente?")).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Cancelar/ })).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Eliminar componente/ })).toBeInTheDocument(); + + await user.click(await screen.findByRole("button", { name: "Close" })); + + await waitFor(() => { + expect(screen.queryByText("¿Está seguro de que desea borrar este componente?")).not.toBeInTheDocument(); + expect(screen.queryByText("AMD Ryzen 7 7800X3D")).toBeInTheDocument(); + }); + + }); + + it("deletes component", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/login", Component: Login }, + { path: "/", Component: () => }, + { path: "/components/:id", Component: ComponentDetail, loader: clientLoader }, + { path: "/components", Component: Components, + loader: async ({ request }) => { + const url = new URL(request.url); + const page = Number(url.searchParams.get("page") ?? 0); + const result = await getComponents(page); + return { items: result.items, hasNext: result.hasNext }; + } + } + ]); + + render(); + + await user.type(screen.getByLabelText("Correo electrónico:"), "admin@example.com"); + await user.type(screen.getByLabelText("Contraseña:"), "adminpass"); + await user.click(screen.getByRole("button", { name: /Iniciar sesión/i })); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + expect(await screen.findByText("¿Está seguro de que desea borrar este componente?")).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Cancelar/ })).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Eliminar componente/ })).toBeInTheDocument(); + + await user.click(await screen.findByRole("button", { name: /Eliminar componente/ })); + + await waitFor(() => { + expect(screen.queryByText("Seagate BarraCuda 3.5")).not.toBeInTheDocument(); + }); + + }); + }); \ No newline at end of file diff --git a/frontend/tests/unit/ComponentDetailPage.test.tsx b/frontend/tests/unit/ComponentDetailPage.test.tsx index a9cf287..2033e67 100644 --- a/frontend/tests/unit/ComponentDetailPage.test.tsx +++ b/frontend/tests/unit/ComponentDetailPage.test.tsx @@ -3,12 +3,15 @@ import { beforeEach, describe, it, expect, vi } from "vitest"; import "@testing-library/jest-dom"; import { createRoutesStub } from "react-router"; import ComponentDetail, { clientLoader } from "~/routes/detail-component"; -import { getComponent } from "~/services/components-service"; +import { getComponent, deleteComponent, getComponents } from "~/services/components-service"; import { reqIsLogged } from "~/services/users-service"; import { useUserStore } from "~/stores/user-store"; +import userEvent from "@testing-library/user-event"; +import Components from "~/routes/components"; vi.mock("~/services/components-service", () => ({ getComponent: vi.fn(), + deleteComponent: vi.fn(), })); const component1 = { @@ -169,6 +172,118 @@ describe("ComponentDetailPage", () => { }); + it("deletes component", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/components/:id", Component: ComponentDetail, loader: () => component1 }, + { path: "/components", + Component: Components, + loader: async ({ request }) => { + const url = new URL(request.url); + const page = Number(url.searchParams.get("page") ?? 0); + const result = await getComponents(page); + return { items: result.items, hasNext: result.hasNext }; + } + }, + ]); + + vi.mocked(deleteComponent).mockResolvedValue(); + vi.mocked(reqIsLogged).mockResolvedValue(adminAccount); + useUserStore.setState({ user: adminAccount }); + + render(); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + expect(await screen.findByText("¿Está seguro de que desea borrar este componente?")).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Cancelar/ })).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Eliminar componente/ })).toBeInTheDocument(); + + await user.click(await screen.findByRole("button", { name: /Eliminar componente/ })); + + await waitFor(() => { + expect(deleteComponent).toHaveBeenCalledWith(component1.id); + expect(screen.queryByText(component1.name)).not.toBeInTheDocument(); + }); + + }); + + it("cancel component deletion", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/components/:id", Component: ComponentDetail, loader: () => component1 }, + { path: "/components", + Component: Components, + loader: async ({ request }) => { + const url = new URL(request.url); + const page = Number(url.searchParams.get("page") ?? 0); + const result = await getComponents(page); + return { items: result.items, hasNext: result.hasNext }; + } + }, + ]); + + vi.mocked(deleteComponent).mockResolvedValue(); + vi.mocked(reqIsLogged).mockResolvedValue(adminAccount); + useUserStore.setState({ user: adminAccount }); + + render(); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + expect(await screen.findByText("¿Está seguro de que desea borrar este componente?")).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Cancelar/ })).toBeInTheDocument(); + expect(await screen.findByRole("button", { name: /Eliminar componente/ })).toBeInTheDocument(); + + await user.click(await screen.findByRole("button", { name: /Cancelar/ })); + + await waitFor(() => { + expect(deleteComponent).not.toHaveBeenCalled(); + expect(screen.queryByText(component1.name)).toBeInTheDocument(); + }); + + }); + + it("cancel component deletion with the close button", async () => { + const user = userEvent.setup(); + const RouterStub = createRoutesStub([ + { path: "/components/:id", Component: ComponentDetail, loader: () => component1 }, + { path: "/components", + Component: Components, + loader: async ({ request }) => { + const url = new URL(request.url); + const page = Number(url.searchParams.get("page") ?? 0); + const result = await getComponents(page); + return { items: result.items, hasNext: result.hasNext }; + } + }, + ]); + + vi.mocked(deleteComponent).mockResolvedValue(); + vi.mocked(reqIsLogged).mockResolvedValue(adminAccount); + useUserStore.setState({ user: adminAccount }); + + render(); + + await waitFor(() => { + expect(document.getElementById("delete-component-button")).toBeInTheDocument(); + }); + await user.click(document.getElementById("delete-component-button")!); + + await user.click(await screen.findByRole("button", { name: "Close" })); + + await waitFor(() => { + expect(screen.queryByText("¿Está seguro de que desea borrar este componente?")).not.toBeInTheDocument(); + expect(deleteComponent).not.toHaveBeenCalled(); + }); + }); + it("clientLoader returns the component", async () => { vi.mocked(getComponent).mockResolvedValue(component1); From 39a03d0c76bf2855af580f805cb012273d413747 Mon Sep 17 00:00:00 2001 From: jaimeeg5 Date: Tue, 27 Oct 2026 15:52:49 +0100 Subject: [PATCH 3/3] fix: add another PC component to the database initializer to prevent test failures --- .../pcmod/service/DatabaseInitializer.java | 7 +++++ .../sample_images/corsair-icue-link.jpg | Bin 0 -> 22074 bytes .../pcmod/e2e/api/ComponentsSystemTests.java | 6 ++--- .../pcmod/e2e/api/HomePageSystemTests.java | 6 ++--- .../pcmod/e2e/ui/ComponentsSystemTests.java | 25 +++--------------- .../pcmod/e2e/ui/HomePageSystemTests.java | 8 +++--- .../ComponentDetailPage.integration.test.tsx | 4 +-- .../ComponentsPage.integration.test.tsx | 3 +-- .../integration/HomePage.integration.test.tsx | 2 +- 9 files changed, 24 insertions(+), 37 deletions(-) create mode 100644 backend/pcmod/src/main/resources/sample_images/corsair-icue-link.jpg diff --git a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java index 16ee900..0af5c5b 100644 --- a/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java +++ b/backend/pcmod/src/main/java/es/codeurjcstudents/pcmod/service/DatabaseInitializer.java @@ -164,6 +164,13 @@ public void init() throws IOException { setComponentImage(component11, "/sample_images/kingston-fury.jpg"); componentsRepository.save(component11); + Component component12 = new Component("Corsair iCUE LINK 3500X RGB", + "ENSAMBLAJES SIN LÍMITES. No hace falta ocupar mucho espacio para montar un PC con un diseño de primera. El 3500X tiene espacio para la mayoría de tarjetas gráficas y refrigeradores de CPU grandes, radiadores de 360 mm e incluso placas base EATX, sin ocupar todo el escritorio. Para un diseño realmente impecable, el 3500X es compatible con las placas base ASUS BTF y MSI PROJECT ZERO, manteniendo las conexiones y cables ocultos.\r\n" + // + "INCLUYE VENTILADORES iCUE LINK RX RGB PERFORMANCE. Se incluye un iCUE LINK System Hub y tres ventiladores RX RGB, lo que hacen de este chasis una maravilla lista para montar dentro de un revolucionario ecosistema. iCUE LINK elimina el desorden de los cables y transforma su diseño en una elegante exposición.", + ComponentType.CASE, "Corsair", BigDecimal.valueOf(118.99), 5); + setComponentImage(component12, "/sample_images/corsair-icue-link.jpg"); + componentsRepository.save(component12); + // Sample Users userService.createUser(new User("user", "example", "user_example", "c/example_address 1", "user@example.com", userPasswordEncoded, "REGISTERED_USER")); diff --git a/backend/pcmod/src/main/resources/sample_images/corsair-icue-link.jpg b/backend/pcmod/src/main/resources/sample_images/corsair-icue-link.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fcf5efd76d18a421121322ba74b1edad01dd4ca GIT binary patch literal 22074 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 literal 0 HcmV?d00001 diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java index bd875dd..8bd32fa 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/ComponentsSystemTests.java @@ -36,8 +36,8 @@ public void getComponents() { .when().get("/components/") .then().statusCode(200).contentType(ContentType.JSON) // Global checks - .body("totalElements", equalTo(11)) - .body("content", hasSize(11)) + .body("totalElements", greaterThan(10)) + .body("content", hasSize(greaterThan(10))) .body("content.brand", hasItems("AMD", "Gigabyte", "Corsair", "MSI", "Kingston", "Asus", "Seagate", "Lian Li", "Intel")) .body("content.price", everyItem(greaterThanOrEqualTo(0.0f))) @@ -67,7 +67,7 @@ public void getComponent() { @Test public void getComponentNotFound() { given().header("Content-Type", "application/json") - .when().get("/components/12") + .when().get("/components/13") .then().statusCode(404).contentType(ContentType.JSON) .body("error", equalTo("Not Found")) .body("message", equalTo("Componente no encontrado.")); diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/HomePageSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/HomePageSystemTests.java index 04073ad..4c546f0 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/HomePageSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/api/HomePageSystemTests.java @@ -34,9 +34,9 @@ public void getComponents() { // Global checks .body("size()", equalTo(3)) // Specific checks - .body("[0].name", equalTo("Kingston FURY Beast")) - .body("[1].name", equalTo("Seagate BarraCuda 3.5")) - .body("[2].name", equalTo("AMD Radeon RX 9060 XT DUAL WHITE")); + .body("[0].name", equalTo("Corsair iCUE LINK 3500X RGB")) + .body("[1].name", equalTo("Kingston FURY Beast")) + .body("[2].name", equalTo("Seagate BarraCuda 3.5")); } } diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java index 7a3db19..2e6ef34 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/ComponentsSystemTests.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.math.BigDecimal; import java.time.Duration; import java.util.Map; @@ -18,23 +17,13 @@ import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import es.codeurjcstudents.pcmod.enums.ComponentType; -import es.codeurjcstudents.pcmod.model.Component; -import es.codeurjcstudents.pcmod.repository.ComponentsRepository; @Tag("client-system") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ComponentsSystemTests { private WebDriver driver; private WebDriverWait wait; - @Autowired - private ComponentsRepository componentsRepository; - @BeforeEach public void setupTest() { ChromeOptions options = new ChromeOptions(); @@ -246,14 +235,6 @@ public void componentDetailOptionsAsAdminTest() { @Test public void deleteComponentTest() { - - Component component = new Component("Intel Core i7-12700K", - "12 núcleos y 20 hilos para un rendimiento máximo\r\n" + // - "Cache L3 de 25 MB\r\n" + // - "Compatible con memorias DDR4 y DDR5\r\n" + // - "Frecuencia base 3.6 GHz y turbo hasta 5.0 GHz", - ComponentType.CPU, "Intel", BigDecimal.valueOf(450.00), 5); - component = componentsRepository.save(component); driver.get("http://localhost:5173/login"); wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email"))); @@ -265,7 +246,7 @@ public void deleteComponentTest() { wait.until(ExpectedConditions.urlToBe("http://localhost:5173/")); - driver.get("http://localhost:5173/components/" + component.getId()); + driver.get("http://localhost:5173/components/8"); wait.until(ExpectedConditions.presenceOfElementLocated(By.id("delete-component-button"))); scrollToAndClick(By.id("delete-component-button")); @@ -277,8 +258,8 @@ public void deleteComponentTest() { .contains("Esta acción no se puede deshacer."); scrollToAndClick(By.cssSelector(".modal-footer .pcmod-danger-button")); - driver.get("http://localhost:5173/components?page=2"); - assertThat(driver.getPageSource()).doesNotContain(component.getName()); + driver.get("http://localhost:5173/components?page=0"); + assertThat(driver.getPageSource()).doesNotContain("Intel Core i5-12400F"); } diff --git a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/HomePageSystemTests.java b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/HomePageSystemTests.java index fca1b6e..f56c799 100644 --- a/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/HomePageSystemTests.java +++ b/backend/pcmod/src/test/java/es/codeurjcstudents/pcmod/e2e/ui/HomePageSystemTests.java @@ -57,10 +57,6 @@ public void loadHomePage() { String logoMessage = driver.findElement(By.id("logo")).getAttribute("alt"); assertThat(logoMessage).isEqualTo("PCMod Logo"); - wait.until(ExpectedConditions.presenceOfElementLocated(By.id("component-9"))); - String componentName9 = driver.findElement(By.id("component-9")).getText(); - assertThat(componentName9).isEqualTo("AMD Radeon RX 9060 XT DUAL WHITE"); - wait.until(ExpectedConditions.presenceOfElementLocated(By.id("component-10"))); String componentName10 = driver.findElement(By.id("component-10")).getText(); assertThat(componentName10).isEqualTo("Seagate BarraCuda 3.5"); @@ -69,6 +65,10 @@ public void loadHomePage() { String componentName11 = driver.findElement(By.id("component-11")).getText(); assertThat(componentName11).isEqualTo("Kingston FURY Beast"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("component-12"))); + String componentName12 = driver.findElement(By.id("component-12")).getText(); + assertThat(componentName12).isEqualTo("Corsair iCUE LINK 3500X RGB"); + wait.until(ExpectedConditions.presenceOfElementLocated(By.name("componentsButton"))); assertThat(driver.findElements(By.name("componentsButton"))).isNotEmpty(); diff --git a/frontend/tests/integration/ComponentDetailPage.integration.test.tsx b/frontend/tests/integration/ComponentDetailPage.integration.test.tsx index af824fe..da0bdc5 100644 --- a/frontend/tests/integration/ComponentDetailPage.integration.test.tsx +++ b/frontend/tests/integration/ComponentDetailPage.integration.test.tsx @@ -201,7 +201,7 @@ describe("ComponentDetailPageIntegration", () => { const user = userEvent.setup(); const RouterStub = createRoutesStub([ { path: "/login", Component: Login }, - { path: "/", Component: () => }, + { path: "/", Component: () => }, { path: "/components/:id", Component: ComponentDetail, loader: clientLoader }, { path: "/components", Component: Components, loader: async ({ request }) => { @@ -231,7 +231,7 @@ describe("ComponentDetailPageIntegration", () => { await user.click(await screen.findByRole("button", { name: /Eliminar componente/ })); await waitFor(() => { - expect(screen.queryByText("Seagate BarraCuda 3.5")).not.toBeInTheDocument(); + expect(screen.queryByText("Intel Core i5-12400F")).not.toBeInTheDocument(); }); }); diff --git a/frontend/tests/integration/ComponentsPage.integration.test.tsx b/frontend/tests/integration/ComponentsPage.integration.test.tsx index d5ad5de..0556179 100644 --- a/frontend/tests/integration/ComponentsPage.integration.test.tsx +++ b/frontend/tests/integration/ComponentsPage.integration.test.tsx @@ -44,8 +44,7 @@ describe("ComponentsPageIntegration", () => { await waitFor(() => { const updatedComponentsNames = screen.getAllByRole("heading", { level: 3 }); - expect(updatedComponentsNames).toHaveLength(11); - expect(updatedComponentsNames[10]).toHaveTextContent("Kingston FURY Beast"); + expect(updatedComponentsNames.length).toBeGreaterThan(10); }); const buttonHasNextFalse = screen.queryByRole("button", { name: /Cargar más componentes/i }); diff --git a/frontend/tests/integration/HomePage.integration.test.tsx b/frontend/tests/integration/HomePage.integration.test.tsx index adad74b..73ed43c 100644 --- a/frontend/tests/integration/HomePage.integration.test.tsx +++ b/frontend/tests/integration/HomePage.integration.test.tsx @@ -28,9 +28,9 @@ describe("HomePageIntegration", () => { }); await waitFor(() => { - expect(screen.getByText("AMD Radeon RX 9060 XT DUAL WHITE")).toBeInTheDocument(); expect(screen.getByText("Seagate BarraCuda 3.5")).toBeInTheDocument(); expect(screen.getByText("Kingston FURY Beast")).toBeInTheDocument(); + expect(screen.getByText("Corsair iCUE LINK 3500X RGB")).toBeInTheDocument(); }); expect(screen.queryByRole("button", { name: /Ver todos los componentes/i })).toBeInTheDocument();