diff --git a/client/src/main/java/client/Ingredient/IngredientController.java b/client/src/main/java/client/Ingredient/IngredientController.java index 429fecd..91cce05 100644 --- a/client/src/main/java/client/Ingredient/IngredientController.java +++ b/client/src/main/java/client/Ingredient/IngredientController.java @@ -23,7 +23,7 @@ public class IngredientController { private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client @FXML - private void handleDeleteIngredient(ActionEvent event) { + void handleDeleteIngredient(ActionEvent event) { // Get selected ingredient Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem(); if (selectedIngredient == null) { diff --git a/client/src/test/java/client/Ingredient/IngredientControllerTest.java b/client/src/test/java/client/Ingredient/IngredientControllerTest.java new file mode 100644 index 0000000..d8a4f79 --- /dev/null +++ b/client/src/test/java/client/Ingredient/IngredientControllerTest.java @@ -0,0 +1,174 @@ +package client.Ingredient; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import commons.Ingredient; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@WireMockTest(httpPort = 8080) +class IngredientControllerMockTest { + + private IngredientController controller; + private ListView ingredientListView; + + // starting javaFX and allow use of listview and alert usage + @BeforeAll + static void initJavaFx() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + try { + Platform.startup(latch::countDown); + } catch (IllegalStateException alreadyStarted) { + latch.countDown(); + } + assertTrue(latch.await(3, TimeUnit.SECONDS), "JavaFX Platform failed to start"); + Platform.setImplicitExit(false); + } +//inject fxml fields and create controller + mock UI + @BeforeEach + void setup() throws Exception { + controller = new IngredientController(); + + ingredientListView = new ListView<>(); + ingredientListView.setItems(FXCollections.observableArrayList( + new Ingredient("Bread", 1, 2, 3), + new Ingredient("Cheese", 2, 2, 2), + new Ingredient("Ham", 3, 3, 3) + )); + + setPrivateField(controller, "ingredientListView", ingredientListView); + setPrivateField(controller, "deleteButton", new Button("Delete")); + } + + // pick ingredient -> backend says not in use -> fake delete ingredient + @Test + void deleteingredientwhennotusedcallsusagethendeleteandclearslist() throws Exception { + Ingredient selected = ingredientListView.getItems().get(0); + ingredientListView.getSelectionModel().select(selected); + + stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")) + .willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":0}"))); + + stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId())) + .willReturn(ok())); + + // safe close for show and wait, run controller on JavaFX + try (DialogCloser closer = startDialogCloser()) { + runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent())); + } + + verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))); + verify(deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId()))); + + assertEquals(0, ingredientListView.getItems().size()); + } + + //select ingredient -> if used backend says it and show warning -> safety delete but shouldn't happen + @Test + void deleteIngredientwhenUsedshowsWarningandDoesNotDeleteifDialogClosed() throws Exception { + Ingredient selected = ingredientListView.getItems().get(1); + ingredientListView.getSelectionModel().select(selected); + + stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")) + .willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":2}"))); + + stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId())) + .willReturn(ok())); + + //safe close as if user selected cancel + try (DialogCloser closer = startDialogCloser()) { + runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent())); + } + + // check usage but not delete + verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))); + verify(0, deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId()))); + + assertEquals(3, ingredientListView.getItems().size()); + } + + // fxml helper + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field f = target.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + f.set(target, value); + } + + //controller on JavaFX + private static void runOnFxThreadAndWait(Runnable action) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + action.run(); + } finally { + latch.countDown(); + } + }); + assertTrue(latch.await(8, TimeUnit.SECONDS), "FX action timed out"); + } + + // safe close so that show and wait doesn't wait forever + private static DialogCloser startDialogCloser() { + AtomicBoolean running = new AtomicBoolean(true); + + Thread t = new Thread(() -> { + while (running.get()) { + try { + Thread.sleep(50); + } catch (InterruptedException ignored) { + } + + Platform.runLater(() -> { + for (Window w : Window.getWindows()) { + if (w instanceof Stage stage && stage.isShowing()) { + stage.close(); + } + } + }); + } + }, "javafx-dialog-closer"); + + t.setDaemon(true); + t.start(); + + return new DialogCloser(running); + } + + // dialog closer + private static final class DialogCloser implements AutoCloseable { + private final AtomicBoolean running; + + private DialogCloser(AtomicBoolean running) { + this.running = running; + } + + @Override + public void close() { + running.set(false); + } + } +}