From 953b7782bde9efff0775ccff2250d29ba9456632 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 8 Jul 2025 20:35:58 +0200 Subject: [PATCH] Erweiterungen --- .../emulatorstation/Application.java | 25 +- .../base/domain/AbstractEntity.java | 44 -- .../base/domain/package-info.java | 7 - .../base/ui/view/AdminView.java | 75 ++++ .../base/ui/view/LoginView.java | 64 +++ .../base/ui/view/MainLayout.java | 32 +- .../base/ui/view/MainView.java | 420 +++++++++++++++++- .../base/ui/view/package-info.java | 7 - .../emulatorstation/pojo/ExecResponse.java | 21 + .../taskmanagement/domain/Task.java | 61 --- .../taskmanagement/domain/TaskRepository.java | 12 - .../taskmanagement/domain/package-info.java | 14 - .../taskmanagement/package-info.java | 15 - .../taskmanagement/service/TaskService.java | 43 -- .../taskmanagement/service/package-info.java | 11 - .../taskmanagement/ui/view/TaskListView.java | 37 +- .../taskmanagement/ui/view/package-info.java | 14 - .../java/util/PreferencesKeyValueStore.java | 20 + src/main/java/util/Util.java | 19 + .../MyVaadinServiceInitListener.java | 10 + src/main/resources/application.properties | 3 + .../emulatorstation/ArchitectureTest.java | 55 --- .../emulatorstation/TestApplication.java | 14 - .../TestcontainersConfiguration.java | 10 - .../taskmanagement/service/TaskServiceIT.java | 57 --- 25 files changed, 665 insertions(+), 425 deletions(-) delete mode 100644 src/main/java/de/assecutor/emulatorstation/base/domain/AbstractEntity.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/base/domain/package-info.java create mode 100644 src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java create mode 100644 src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/base/ui/view/package-info.java create mode 100644 src/main/java/de/assecutor/emulatorstation/pojo/ExecResponse.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/Task.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/TaskRepository.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/package-info.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/package-info.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/service/TaskService.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/service/package-info.java delete mode 100644 src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/package-info.java create mode 100644 src/main/java/util/PreferencesKeyValueStore.java create mode 100644 src/main/java/util/Util.java create mode 100644 src/main/resources/MyVaadinServiceInitListener.java delete mode 100644 src/test/java/de/assecutor/emulatorstation/ArchitectureTest.java delete mode 100644 src/test/java/de/assecutor/emulatorstation/TestApplication.java delete mode 100644 src/test/java/de/assecutor/emulatorstation/TestcontainersConfiguration.java delete mode 100644 src/test/java/de/assecutor/emulatorstation/taskmanagement/service/TaskServiceIT.java diff --git a/src/main/java/de/assecutor/emulatorstation/Application.java b/src/main/java/de/assecutor/emulatorstation/Application.java index 92f85d9..721c3f8 100644 --- a/src/main/java/de/assecutor/emulatorstation/Application.java +++ b/src/main/java/de/assecutor/emulatorstation/Application.java @@ -1,24 +1,31 @@ package de.assecutor.emulatorstation; import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.component.page.Push; import com.vaadin.flow.theme.Theme; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -import java.time.Clock; +import java.util.Map; @SpringBootApplication +@Push @Theme("default") public class Application implements AppShellConfigurator { - - @Bean - public Clock clock() { - return Clock.systemDefaultZone(); // You can also use Clock.systemUTC() - } + public static final Map users = Map.ofEntries( + Map.entry("admin", "ZY6X9X93Co8m"), + Map.entry("GFL", "GFL123"), + Map.entry("Berlin", "Berlin123"), + Map.entry("Bremen", "Bremen123"), + Map.entry("Hamburg", "Hamburg123"), + Map.entry("Essen", "Essen123"), + Map.entry("Leipzig", "Leipzig123"), + Map.entry("Dresden", "Dresden123"), + Map.entry("Hannover", "Hannover123"), + Map.entry("Stuttgart", "Stuttgart123"), + Map.entry("FrankfurtAmMain", "FrankfurtAmMain123") + ); public static void main(String[] args) { SpringApplication.run(Application.class, args); } - } diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/AbstractEntity.java b/src/main/java/de/assecutor/emulatorstation/base/domain/AbstractEntity.java deleted file mode 100644 index 7f9dee5..0000000 --- a/src/main/java/de/assecutor/emulatorstation/base/domain/AbstractEntity.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.assecutor.emulatorstation.base.domain; - -import jakarta.persistence.MappedSuperclass; -import org.jspecify.annotations.Nullable; -import org.springframework.data.util.ProxyUtils; - -@MappedSuperclass -public abstract class AbstractEntity { - - public abstract @Nullable ID getId(); - - @Override - public String toString() { - return "%s{id=%s}".formatted(getClass().getSimpleName(), getId()); - } - - @Override - public int hashCode() { - // Hashcode should never change during the lifetime of an object. Because of - // this we can't use getId() to calculate the hashcode. Unless you have sets - // with lots of entities in them, returning the same hashcode should not be a - // problem. - return ProxyUtils.getUserClass(getClass()).hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } else if (obj == this) { - return true; - } - - var thisUserClass = ProxyUtils.getUserClass(getClass()); - var otherUserClass = ProxyUtils.getUserClass(obj); - if (thisUserClass != otherUserClass) { - return false; - } - - var id = getId(); - return id != null && id.equals(((AbstractEntity) obj).getId()); - } - -} diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/package-info.java b/src/main/java/de/assecutor/emulatorstation/base/domain/package-info.java deleted file mode 100644 index ed183f7..0000000 --- a/src/main/java/de/assecutor/emulatorstation/base/domain/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This package contains reusable domain classes. - */ -@NullMarked -package de.assecutor.emulatorstation.base.domain; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java new file mode 100644 index 0000000..d8753f2 --- /dev/null +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/AdminView.java @@ -0,0 +1,75 @@ +package de.assecutor.emulatorstation.base.ui.view; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Menu; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.theme.lumo.LumoUtility; +import jakarta.annotation.security.PermitAll; +import util.PreferencesKeyValueStore; +import util.Util; + +@Route("admin") +@PageTitle("Admin") +@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Admin") +@PermitAll // When security is enabled, allow all authenticated users +public class AdminView extends Main { + final TextField serverPortTextView; + final Button saveBtn; + final Button logoutBtn; + + final PreferencesKeyValueStore preferences = new PreferencesKeyValueStore(); + + public AdminView() { + addClassName(LumoUtility.Padding.MEDIUM); + + var verticalLayout = new VerticalLayout(); + add(verticalLayout); + + serverPortTextView = new TextField(); + serverPortTextView.setPlaceholder("Server"); + serverPortTextView.setValue("172.16.0.158"); + serverPortTextView.setMinWidth("20em"); + verticalLayout.add(serverPortTextView); + + var serverPort = preferences.get("server"); + serverPort.ifPresent(serverPortTextView::setValue); + + var horizontalLayout = new HorizontalLayout(); + add(horizontalLayout); + + saveBtn = new Button("Save", event -> savePreferences()); + saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + horizontalLayout.add(saveBtn); + + logoutBtn = new Button("Logout", event -> logout()); + logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + horizontalLayout.add(logoutBtn); + + setSizeFull(); + } + + private void logout() { + VaadinService.getCurrentRequest().getWrappedSession().invalidate(); + + // Navigiere den Benutzer zur Login-Seite + UI.getCurrent().navigate(""); + } + + private void savePreferences() { + var preferences = new PreferencesKeyValueStore(); + + preferences.put("server", serverPortTextView.getValue()); + + Notification.show("Daten gespeichert!", 3000, Notification.Position.MIDDLE); + + } +} diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java new file mode 100644 index 0000000..628ef17 --- /dev/null +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/LoginView.java @@ -0,0 +1,64 @@ +package de.assecutor.emulatorstation.base.ui.view; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinSession; +import de.assecutor.emulatorstation.Application; + +@Route("") +public class LoginView extends VerticalLayout { + + public LoginView() { + setAlignItems(Alignment.CENTER); + + Span title = new Span("Anmelden"); + title.getStyle().set("font-size", "24px").set("font-weight", "bold"); + + TextField usernameField = new TextField("Benutzername"); + PasswordField passwordField = new PasswordField("Passwort"); + + Button loginButton = new Button("Login", event -> { + String username = usernameField.getValue(); + String password = passwordField.getValue(); + + if (authenticate(username, password)) { + var currentSession = VaadinSession.getCurrent().getSession(); + currentSession.setMaxInactiveInterval(60); // Timeout in Sekunden + currentSession.setAttribute("username", username); + + VaadinService.getCurrentRequest().getWrappedSession().setAttribute("user", username); + Notification.show("Anmeldung erfolgreich!"); + + if (username.equals("admin")) { + getUI().ifPresent(ui -> ui.navigate(AdminView.class)); + } else { + getUI().ifPresent(ui -> ui.navigate(MainView.class)); + } + } else { + Notification.show("Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Daten!", 3000, Notification.Position.MIDDLE); + } + }); + + loginButton.getStyle().set("margin-top", "10px"); + add(title, usernameField, passwordField, loginButton); + + MainLayout.instance.setDrawerOpened(false); + } + + // Einfache Authentifizierung (hier nur als Beispiel, später durch echte Logik ersetzen) + private boolean authenticate(String username, String password) { + if (Application.users.containsKey(username)) { + var userPassword = Application.users.get(username); + + return userPassword.equals(password); + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java index d72561f..21e6c9f 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainLayout.java @@ -1,6 +1,7 @@ package de.assecutor.emulatorstation.base.ui.view; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.avatar.AvatarVariant; @@ -14,6 +15,7 @@ import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.router.Layout; +import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.menu.MenuConfiguration; import com.vaadin.flow.server.menu.MenuEntry; import jakarta.annotation.security.PermitAll; @@ -23,12 +25,41 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*; @Layout @PermitAll // When security is enabled, allow all authenticated users public final class MainLayout extends AppLayout { + public static MainLayout instance = null; MainLayout() { + instance = this; + + startPolling(); + setPrimarySection(Section.DRAWER); addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu()); + + setDrawerOpened(false); } + private void startPolling() { + UI ui = UI.getCurrent(); + ui.setPollInterval(60000); // Poll-Intervall auf 60 Sekunden setzen + + // Session expiration handling + ui.addPollListener(event -> { + if (!isUserSessionValid()) { + VaadinSession.getCurrent().getSession().invalidate(); // Session invalidieren + ui.access(() -> ui.navigate("login")); // Benutzer zur Login-Seite weiterleiten + } + }); + } + + private boolean isUserSessionValid() { + // Logik für Sitzungsprüfung, z.B. Timeout-Zeit überprüfen + return VaadinSession.getCurrent() != null + && VaadinSession.getCurrent().getSession() != null + && VaadinSession.getCurrent().getSession().getLastAccessedTime() + 300000 + > System.currentTimeMillis(); // 300000 ms = 5 Minuten + } + + private Div createHeader() { // TODO Replace with real application logo and name var appLogo = VaadinIcon.CUBES.create(); @@ -76,5 +107,4 @@ public final class MainLayout extends AppLayout { return userMenu; } - } diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java index 99fa1b5..183c3dc 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java @@ -1,33 +1,423 @@ package de.assecutor.emulatorstation.base.ui.view; -import de.assecutor.emulatorstation.base.ui.component.ViewToolbar; +import com.vaadin.flow.component.ClientCallable; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.IFrame; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Main; import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; +import de.assecutor.emulatorstation.pojo.ExecResponse; import jakarta.annotation.security.PermitAll; +import util.Util; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -/** - * This view shows up when a user navigates to the root ('/') of the application. - */ -@Route @PermitAll // When security is enabled, allow all authenticated users -public final class MainView extends Main { +@Route("main") +public final class MainView extends Main implements BeforeEnterObserver +{ + private final String username; + private final String server; - // TODO Replace with your own main view. + private final IFrame webView; + + private UI ui; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); MainView() { + try { + } catch (Exception ex) { + showWaitDialog("Fehler!\n" + ex.toString()); + } + + var currentSession = VaadinService.getCurrentRequest().getWrappedSession(); + username = Util.getSessionAttributeWithDefault(currentSession, "username", null); + server = Util.getSessionAttributeWithDefault(currentSession, "server", "172.16.0.158"); + addClassName(LumoUtility.Padding.MEDIUM); - add(new ViewToolbar("Main")); - add(new Div("Please select a view from the menu on the left.")); + + String jsCode = """ + var inactivityTime = function () { + var time; + window.onload = resetTimer; + document.onmousemove = resetTimer; + document.onkeypress = resetTimer; + document.onclick = resetTimer; + document.onscroll = resetTimer; + + function logout() { + $0.$server.logout(); // Server-Logout via RPC + } + + function resetTimer() { + clearTimeout(time); + time = setTimeout(logout, 5 * 60 * 1000); // 5 Minute Inaktivität + } + }; + inactivityTime(); + """; + + UI.getCurrent().getPage().executeJs(jsCode, this); + + var horizontalLayout = new HorizontalLayout(); + add(horizontalLayout); + + Button startBtn = new Button("Start", event -> startup()); + startBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + horizontalLayout.add(startBtn); + + Button logoutBtn = new Button("Logout", event -> logout()); + logoutBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + horizontalLayout.add(logoutBtn); + + webView = new IFrame(); + webView.setSizeFull(); // IFrame auf volle Größe setzen + webView.getElement().setAttribute("frameborder", "0"); // Optional: Rahmen entfernen + + // Layout konfigurieren + setSizeFull(); // Vertikales Layout auf volle Größe setzen + add(webView); + + addAttachListener(event -> { + ui = event.getUI(); + }); } - /** - * Navigates to the main view. - */ - public static void showMainView() { - UI.getCurrent().navigate(MainView.class); + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + String user = (String) VaadinService.getCurrentRequest().getWrappedSession().getAttribute("user"); + if (user == null) { + beforeEnterEvent.rerouteTo("login"); + } + } + + private void startup() { + UI ui = UI.getCurrent(); + + var dialog = showWaitDialog("Der Emulator wird gestartet, bitte warten..."); + + executor.submit(() -> { + try { + shutdown(); + + createContainer(); + + startContainer(); + + waitContainerStart(); + + downloadApp(); + + installApp(); + + } catch (Exception ex) { + // Logging, Fehlerbehandlung … + } finally { + // 4) UI-update sicher aus Nicht-UI-Thread heraus + ui.access(() -> { + dialog.close(); + + refreshWebView(); + }); + } + }); + } + + private void shutdown() { + stopContainer(); + + deleteContainer(); + } + + private void refreshWebView() { + webView.setSrc("http://" + server + ":6080/?autoconnect=true"); + webView.reload(); + } + + private void downloadApp() { + try { + HttpClient client = HttpClient.newHttpClient(); + + String jsonPayload = """ + { + "Cmd": ["curl", "-O", "https://www.appcreation.de/download/sb.apk"], + "AttachStdout": true, + "AttachStderr": true + }"""; + + // HTTP-Request erstellen + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + // Anfrage senden und Antwort verarbeiten + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); + System.out.println("Response-Body: " + response.body()); + + var execId = ExecResponse.parse(response.body()); + + exec(execId); + } catch (Exception e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void installApp() { + HttpClient client = HttpClient.newHttpClient(); + + String jsonPayload = """ + { + "Cmd": ["adb", "install", "sb.apk"], + "AttachStdout": true, + "AttachStderr": true + }"""; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); + System.out.println("Response-Body: " + response.body()); + + var execId = ExecResponse.parse(response.body()); + + exec(execId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private String exec(String execId) { + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/exec/" + execId + "/start")) + .POST(HttpRequest.BodyPublishers.noBody()) + .version(HttpClient.Version.HTTP_1_1) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben + System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben + + return response.body(); + } catch (Exception e) { + e.printStackTrace(); // Fehler behandeln + } + + return null; + } + + private void startContainer() { + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/start")) + .POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben + System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben + } catch (Exception e) { + e.printStackTrace(); // Fehler behandeln + } + } + + private void waitContainerStart() { + boolean endLoop = false; + + do { + try { + HttpClient client = HttpClient.newHttpClient(); + + String jsonPayload = """ + { + "Cmd": ["cat", "device_status"], + "AttachStdout": true, + "AttachStderr": true + }"""; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/exec")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); + System.out.println("Response-Body: " + response.body()); + + var execId = ExecResponse.parse(response.body()); + + var execResponse = exec(execId); + if (execResponse != null) { + var trimmedResponse = execResponse.trim(); + + if (trimmedResponse.equals("READY")) { + endLoop = true; + } else { + Thread.sleep(1000); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } while (!endLoop); + } + + private void createContainer() { + HttpClient client = HttpClient.newHttpClient(); + + // JSON-Payload für die Anfrage + String jsonPayload = """ + { + "Image": "budtmo/docker-android:emulator_11.0", + "Env": [ + "EMULATOR_DEVICE=Samsung Galaxy S10", + "WEB_VNC=true" + ], + "HostConfig": { + "PortBindings": { + "6080/tcp": [ + { "HostPort": "6080" } + ] + }, + "Devices": [ + { + "PathOnHost": "/dev/kvm", + "PathInContainer": "/dev/kvm", + "CgroupPermissions": "rwm" + } + ] + }, + "ExposedPorts": { + "6080/tcp": {} + } + } + """; + + // HTTP-Request erstellen + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/create?name=android-container-" + username)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + // Anfrage senden und Antwort verarbeiten + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); + System.out.println("Response-Body: " + response.body()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void stopContainer() { + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "/stop")) + .POST(HttpRequest.BodyPublishers.noBody()) // Kein Body wird gesendet + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben + System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben + } catch (Exception e) { + e.printStackTrace(); // Fehler behandeln + } + } + + private void deleteContainer() { + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://" + server + ":2375/containers/android-container-" + username + "?force=true")) + .DELETE() // Kein Body wird gesendet + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("HTTP-Response-Code: " + response.statusCode()); // Statuscode ausgeben + System.out.println("Response-Body: " + response.body()); // Antwort-Body ausgeben + } catch (Exception e) { + e.printStackTrace(); // Fehler behandeln + } + } + + private Dialog showWaitDialog(String message) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Bitte warten"); + dialog.setModal(true); + dialog.setCloseOnOutsideClick(false); + + VerticalLayout dialogLayout = new VerticalLayout( + new Paragraph(message) + ); + dialog.add(dialogLayout); + + dialog.open(); + + return dialog; + } + + @ClientCallable + public void logout() { + UI ui = UI.getCurrent(); + var vaadinSession = VaadinService.getCurrentRequest().getWrappedSession(); + + var dialog = showWaitDialog("Sie werden abgemeldet. Bitte warten..."); + dialog.open(); + + executor.submit(() -> { + try { + shutdown(); + } catch (Exception ex) { + // Logging, Fehlerbehandlung … + } finally { + // 4) UI-update sicher aus Nicht-UI-Thread heraus + ui.access(() -> { + dialog.close(); + + vaadinSession.invalidate(); + + // Navigiere den Benutzer zur Login-Seite + ui.navigate(""); + }); + } + }); } } diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/package-info.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/package-info.java deleted file mode 100644 index 6328ab9..0000000 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This package contains reusable or cross-cutting view-related classes. - */ -@NullMarked -package de.assecutor.emulatorstation.base.ui.view; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/de/assecutor/emulatorstation/pojo/ExecResponse.java b/src/main/java/de/assecutor/emulatorstation/pojo/ExecResponse.java new file mode 100644 index 0000000..0ab648e --- /dev/null +++ b/src/main/java/de/assecutor/emulatorstation/pojo/ExecResponse.java @@ -0,0 +1,21 @@ +package de.assecutor.emulatorstation.pojo; + +import com.fasterxml.jackson.annotation.JsonKey; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ExecResponse { + @com.fasterxml.jackson.annotation.JsonProperty("Id") + private String id; + + public static String parse(String json) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + var value = objectMapper.readValue(json, ExecResponse.class); + + return value.getId(); + } + + // Getter und Setter + public String getId() { return id; } + public void setId(String id) { this.id = id; } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/Task.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/Task.java deleted file mode 100644 index caa9d0b..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/Task.java +++ /dev/null @@ -1,61 +0,0 @@ -package de.assecutor.emulatorstation.taskmanagement.domain; - -import de.assecutor.emulatorstation.base.domain.AbstractEntity; -import jakarta.persistence.*; -import jakarta.validation.constraints.Size; -import org.jspecify.annotations.Nullable; - -import java.time.Instant; -import java.time.LocalDate; - -@Entity -@Table(name = "task") -public class Task extends AbstractEntity { - - public static final int DESCRIPTION_MAX_LENGTH = 255; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "task_id") - private Long id; - - @Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH) - @Size(max = DESCRIPTION_MAX_LENGTH) - private String description; - - @Column(name = "creation_date", nullable = false) - private Instant creationDate; - - @Column(name = "due_date") - @Nullable - private LocalDate dueDate; - - @Override - public @Nullable Long getId() { - return id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Instant getCreationDate() { - return creationDate; - } - - public void setCreationDate(Instant creationDate) { - this.creationDate = creationDate; - } - - public @Nullable LocalDate getDueDate() { - return dueDate; - } - - public void setDueDate(@Nullable LocalDate dueDate) { - this.dueDate = dueDate; - } -} diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/TaskRepository.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/TaskRepository.java deleted file mode 100644 index d03b5a0..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/TaskRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.assecutor.emulatorstation.taskmanagement.domain; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; - -public interface TaskRepository extends JpaRepository, JpaSpecificationExecutor { - - // If you don't need a total row count, Slice is better than Page. - Slice findAllBy(Pageable pageable); -} diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/package-info.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/package-info.java deleted file mode 100644 index 6a8420d..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/domain/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * This package contains the domain model of the Task Management sample feature. - *

- * You can add as many domain model artifacts (such as entities, value objects, repositories, domain events, and domain - * services) to this package, as long as they belong to the same feature. - *

- *

- * If you have domain classes that are re-usable across multiple features, add them to the {@code base.domain} package. - *

- */ -@NullMarked -package de.assecutor.emulatorstation.taskmanagement.domain; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/package-info.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/package-info.java deleted file mode 100644 index bea69b1..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/package-info.java +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This is a feature package for the Task Management sample feature. Its purpose is to demonstrate how you typically - * structure Vaadin business applications, and how the different building blocks interact. - *

- * A feature package represents a self-contained unit of functionality, including UI components, business logic, and - * data access. It could be a subdomain or bounded context (e.g., "Billing"), a specific use case (e.g., "User - * Registration"), or even a complex UI view (e.g., "Dashboard"). - *

- *

- * If your application is very small, you may not need dedicated feature packages. In that case, move the subpackages - * directly to the application package. - *

- */ -package de.assecutor.emulatorstation.taskmanagement; -// TODO Remove this package once you have added real features diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/TaskService.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/TaskService.java deleted file mode 100644 index 6a2ab2d..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/TaskService.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.assecutor.emulatorstation.taskmanagement.service; - -import de.assecutor.emulatorstation.taskmanagement.domain.Task; -import de.assecutor.emulatorstation.taskmanagement.domain.TaskRepository; -import org.jspecify.annotations.Nullable; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Clock; -import java.time.LocalDate; -import java.util.List; - -@Service -@Transactional(propagation = Propagation.REQUIRES_NEW) -public class TaskService { - - private final TaskRepository taskRepository; - - private final Clock clock; - - TaskService(TaskRepository taskRepository, Clock clock) { - this.taskRepository = taskRepository; - this.clock = clock; - } - - public void createTask(String description, @Nullable LocalDate dueDate) { - if ("fail".equals(description)) { - throw new RuntimeException("This is for testing the error handler"); - } - var task = new Task(); - task.setDescription(description); - task.setCreationDate(clock.instant()); - task.setDueDate(dueDate); - taskRepository.saveAndFlush(task); - } - - public List list(Pageable pageable) { - return taskRepository.findAllBy(pageable).toList(); - } - -} diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/package-info.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/package-info.java deleted file mode 100644 index c61a894..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/service/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This package contains the application services of the Task Management sample feature. - *

- * You can add as many application services as you want to this package, as long as they belong to the same feature. If - * any of them use Data Transfer Objects (DTO), you can add them here as well. - *

- */ -@NullMarked -package de.assecutor.emulatorstation.taskmanagement.service; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/TaskListView.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/TaskListView.java index e101c27..cd8027a 100644 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/TaskListView.java +++ b/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/TaskListView.java @@ -1,12 +1,9 @@ package de.assecutor.emulatorstation.taskmanagement.ui.view; import de.assecutor.emulatorstation.base.ui.component.ViewToolbar; -import de.assecutor.emulatorstation.taskmanagement.domain.Task; -import de.assecutor.emulatorstation.taskmanagement.service.TaskService; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.datepicker.DatePicker; -import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; @@ -18,32 +15,20 @@ import com.vaadin.flow.theme.lumo.LumoUtility; import jakarta.annotation.security.PermitAll; import java.time.Clock; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.Optional; - -import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest; @Route("task-list") @PageTitle("Task List") @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List") @PermitAll // When security is enabled, allow all authenticated users public class TaskListView extends Main { - - private final TaskService taskService; - final TextField description; final DatePicker dueDate; final Button createBtn; - final Grid taskGrid; - - public TaskListView(TaskService taskService, Clock clock) { - this.taskService = taskService; + public TaskListView() { description = new TextField(); description.setPlaceholder("What do you want to do?"); description.setAriaLabel("Task description"); - description.setMaxLength(Task.DESCRIPTION_MAX_LENGTH); description.setMinWidth("20em"); dueDate = new DatePicker(); @@ -53,33 +38,13 @@ public class TaskListView extends Main { createBtn = new Button("Create", event -> createTask()); createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(clock.getZone()) - .withLocale(getLocale()); - var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale()); - - taskGrid = new Grid<>(); - taskGrid.setItems(query -> taskService.list(toSpringPageRequest(query)).stream()); - taskGrid.addColumn(Task::getDescription).setHeader("Description"); - taskGrid.addColumn(task -> Optional.ofNullable(task.getDueDate()).map(dateFormatter::format).orElse("Never")) - .setHeader("Due Date"); - taskGrid.addColumn(task -> dateTimeFormatter.format(task.getCreationDate())).setHeader("Creation Date"); - taskGrid.setSizeFull(); - setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); add(new ViewToolbar("Task List", ViewToolbar.group(description, dueDate, createBtn))); - add(taskGrid); } private void createTask() { - taskService.createTask(description.getValue(), dueDate.getValue()); - taskGrid.getDataProvider().refreshAll(); - description.clear(); - dueDate.clear(); - Notification.show("Task added", 3000, Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); } - } diff --git a/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/package-info.java b/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/package-info.java deleted file mode 100644 index 84a09a5..0000000 --- a/src/main/java/de/assecutor/emulatorstation/taskmanagement/ui/view/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * This package contains the views of the Task Management sample feature. - *

- * You can add as many views as you want to this package, as long as they belong to the same feature. - *

- *

- * For smaller UI-components, consider creating a separate {@code ui.component} package. If the components are re-usable - * across multiple features, add them to the {@code base.ui.component} package. - *

- */ -@NullMarked -package de.assecutor.emulatorstation.taskmanagement.ui.view; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/util/PreferencesKeyValueStore.java b/src/main/java/util/PreferencesKeyValueStore.java new file mode 100644 index 0000000..894f428 --- /dev/null +++ b/src/main/java/util/PreferencesKeyValueStore.java @@ -0,0 +1,20 @@ +package util; + +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.prefs.Preferences; + +@Service +public class PreferencesKeyValueStore { + + private final Preferences prefs = Preferences.userRoot().node("emulatorstation"); + + public void put(String key, String value) { + prefs.put(key, value); + } + + public Optional get(String key) { + return Optional.ofNullable(prefs.get(key, null)); + } +} diff --git a/src/main/java/util/Util.java b/src/main/java/util/Util.java new file mode 100644 index 0000000..fa6e958 --- /dev/null +++ b/src/main/java/util/Util.java @@ -0,0 +1,19 @@ +package util; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.WrappedSession; + + +public class Util { + public static String getSessionAttributeWithDefault(WrappedSession currentSession, String key, String defaultValue) { + var result = defaultValue; + + try { + result = currentSession.getAttribute(key).toString(); + } catch (Exception e) {} + + return result; + } +} diff --git a/src/main/resources/MyVaadinServiceInitListener.java b/src/main/resources/MyVaadinServiceInitListener.java new file mode 100644 index 0000000..1678add --- /dev/null +++ b/src/main/resources/MyVaadinServiceInitListener.java @@ -0,0 +1,10 @@ +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.VaadinServiceInitListener; + +public class MyVaadinServiceInitListener implements VaadinServiceInitListener { + @Override + public void serviceInit(ServiceInitEvent event) { + // Setze das global für den Service gültige Heartbeat-Interval + event.getSource().setHeartbeatInterval(300); // 5 Minuten (300 Sekunden) + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8dd219e..b4dac42 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,3 +12,6 @@ spring.jpa.open-in-view=false # Initialize the JPA Entity Manager before considering data.sql so that the EM can create the schema and data.sql contain data spring.jpa.defer-datasource-initialization = true + +server.servlet.session.timeout=300s +vaadin.heartbeatInterval=300 diff --git a/src/test/java/de/assecutor/emulatorstation/ArchitectureTest.java b/src/test/java/de/assecutor/emulatorstation/ArchitectureTest.java deleted file mode 100644 index f258a7f..0000000 --- a/src/test/java/de/assecutor/emulatorstation/ArchitectureTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package de.assecutor.emulatorstation; - -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import org.junit.jupiter.api.Test; -import org.springframework.data.repository.Repository; -import org.springframework.transaction.annotation.Transactional; - -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; -import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; - -class ArchitectureTest { - - static final String BASE_PACKAGE = "de.assecutor.emulatorstation"; - - private final JavaClasses importedClasses = new ClassFileImporter().importPackages(BASE_PACKAGE); - - // TODO Add your own rules and remove those that don't apply to your project - - @Test - void domain_model_should_not_depend_on_application_services() { - noClasses().that().resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat() - .resideInAPackage(BASE_PACKAGE + "..service..").check(importedClasses); - } - - @Test - void domain_model_should_not_depend_on_the_user_interface() { - noClasses().that().resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat() - .resideInAnyPackage(BASE_PACKAGE + "..ui..").check(importedClasses); - } - - @Test - void repositories_should_only_be_used_by_application_services_and_other_domain_classes() { - classes().that().areAssignableTo(Repository.class).should().onlyHaveDependentClassesThat() - .resideInAnyPackage(BASE_PACKAGE + "..domain..", BASE_PACKAGE + "..service..").check(importedClasses); - } - - @Test - void repositories_should_only_be_accessed_by_transactional_classes() { - classes().that().areAssignableTo(Repository.class).should().onlyBeAccessed().byClassesThat() - .areAnnotatedWith(Transactional.class).check(importedClasses); - } - - @Test - void application_services_should_not_depend_on_the_user_interface() { - noClasses().that().resideInAPackage(BASE_PACKAGE + "..service..").should().dependOnClassesThat() - .resideInAnyPackage(BASE_PACKAGE + "..ui..").check(importedClasses); - } - - @Test - void there_should_not_be_circular_dependencies_between_feature_packages() { - slices().matching(BASE_PACKAGE + ".(*)..").should().beFreeOfCycles().check(importedClasses); - } -} diff --git a/src/test/java/de/assecutor/emulatorstation/TestApplication.java b/src/test/java/de/assecutor/emulatorstation/TestApplication.java deleted file mode 100644 index 6965af0..0000000 --- a/src/test/java/de/assecutor/emulatorstation/TestApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.assecutor.emulatorstation; - -import org.springframework.boot.SpringApplication; - -/** - * Run this application class to start your application locally, using Testcontainers for all external services. You - * have to configure the containers in {@link TestcontainersConfiguration}. - */ -public class TestApplication { - - public static void main(String[] args) { - SpringApplication.from(Application::main).with(TestcontainersConfiguration.class).run(args); - } -} diff --git a/src/test/java/de/assecutor/emulatorstation/TestcontainersConfiguration.java b/src/test/java/de/assecutor/emulatorstation/TestcontainersConfiguration.java deleted file mode 100644 index a636313..0000000 --- a/src/test/java/de/assecutor/emulatorstation/TestcontainersConfiguration.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.assecutor.emulatorstation; - -import org.springframework.boot.test.context.TestConfiguration; - -@TestConfiguration(proxyBeanMethods = false) -public class TestcontainersConfiguration { - - // TODO Configure your Testcontainers here. - // See https://docs.spring.io/spring-boot/reference/testing/testcontainers.html for details. -} diff --git a/src/test/java/de/assecutor/emulatorstation/taskmanagement/service/TaskServiceIT.java b/src/test/java/de/assecutor/emulatorstation/taskmanagement/service/TaskServiceIT.java deleted file mode 100644 index 1eaed02..0000000 --- a/src/test/java/de/assecutor/emulatorstation/taskmanagement/service/TaskServiceIT.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.assecutor.emulatorstation.taskmanagement.service; - -import de.assecutor.emulatorstation.TestcontainersConfiguration; -import de.assecutor.emulatorstation.taskmanagement.domain.Task; -import de.assecutor.emulatorstation.taskmanagement.domain.TaskRepository; -import jakarta.validation.ValidationException; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.PageRequest; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Clock; -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@Import(TestcontainersConfiguration.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) -@Transactional(propagation = Propagation.NOT_SUPPORTED) -class TaskServiceIT { - - @Autowired - TaskService taskService; - - @Autowired - TaskRepository taskRepository; - - @Autowired - Clock clock; - - @AfterEach - void cleanUp() { - taskRepository.deleteAll(); - } - - @Test - public void tasks_are_stored_in_the_database_with_the_current_timestamp() { - var now = clock.instant(); - var due = LocalDate.of(2025, 2, 7); - taskService.createTask("Do this", due); - assertThat(taskService.list(PageRequest.ofSize(1))).singleElement() - .matches(task -> task.getDescription().equals("Do this") && due.equals(task.getDueDate()) - && task.getCreationDate().isAfter(now)); - } - - @Test - public void tasks_are_validated_before_they_are_stored() { - assertThatThrownBy(() -> taskService.createTask("X".repeat(Task.DESCRIPTION_MAX_LENGTH + 1), null)) - .isInstanceOf(ValidationException.class); - assertThat(taskRepository.count()).isEqualTo(0); - } -}