Generated project

This commit is contained in:
start.vaadin.com
2025-06-11 09:11:05 +00:00
commit aaf4e45f9d
35 changed files with 1697 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
{
"lumoImports": [
"typography",
"color",
"spacing",
"badge",
"utility"
]
}

View File

@@ -0,0 +1,24 @@
package de.assecutor.emulatorstation;
import com.vaadin.flow.component.page.AppShellConfigurator;
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;
@SpringBootApplication
@Theme("default")
public class Application implements AppShellConfigurator {
@Bean
public Clock clock() {
return Clock.systemDefaultZone(); // You can also use Clock.systemUTC()
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,44 @@
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<ID> {
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());
}
}

View File

@@ -0,0 +1,7 @@
/**
* This package contains reusable domain classes.
*/
@NullMarked
package de.assecutor.emulatorstation.base.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,41 @@
package de.assecutor.emulatorstation.base.ui.component;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.theme.lumo.LumoUtility.*;
public final class ViewToolbar extends Composite<Header> {
public ViewToolbar(String viewTitle, Component... components) {
addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, AlignItems.STRETCH, Gap.MEDIUM,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
var drawerToggle = new DrawerToggle();
drawerToggle.addClassNames(Margin.NONE);
var title = new H1(viewTitle);
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
var toggleAndTitle = new Div(drawerToggle, title);
toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER);
getContent().add(toggleAndTitle);
if (components.length > 0) {
var actions = new Div(components);
actions.addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, Flex.GROW, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW);
getContent().add(actions);
}
}
public static Component group(Component... components) {
var group = new Div(components);
group.addClassNames(Display.FLEX, FlexDirection.COLUMN, AlignItems.STRETCH, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
return group;
}
}

View File

@@ -0,0 +1,7 @@
/**
* This package contains reusable UI components.
*/
@NullMarked
package de.assecutor.emulatorstation.base.ui.component;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,32 @@
package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.server.VaadinServiceInitListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class MainErrorHandler {
private static final Logger log = LoggerFactory.getLogger(MainErrorHandler.class);
@Bean
public VaadinServiceInitListener errorHandlerInitializer() {
return (event) -> event.getSource().addSessionInitListener(
sessionInitEvent -> sessionInitEvent.getSession().setErrorHandler(errorEvent -> {
log.error("An unexpected error occurred", errorEvent.getThrowable());
errorEvent.getComponent().flatMap(Component::getUI).ifPresent(ui -> {
var notification = new Notification(
"An unexpected error has occurred. Please try again later.");
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
notification.setPosition(Notification.Position.TOP_CENTER);
notification.setDuration(3000);
ui.access(notification::open);
});
}));
}
}

View File

@@ -0,0 +1,80 @@
package de.assecutor.emulatorstation.base.ui.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
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.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry;
import jakarta.annotation.security.PermitAll;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@Layout
@PermitAll // When security is enabled, allow all authenticated users
public final class MainLayout extends AppLayout {
MainLayout() {
setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu());
}
private Div createHeader() {
// TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
var appName = new Span("Emulatorstation");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
return header;
}
private SideNav createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
MenuConfiguration.getMenuEntries().forEach(entry -> nav.addItem(createSideNavItem(entry)));
return nav;
}
private SideNavItem createSideNavItem(MenuEntry menuEntry) {
if (menuEntry.icon() != null) {
return new SideNavItem(menuEntry.title(), menuEntry.path(), new Icon(menuEntry.icon()));
} else {
return new SideNavItem(menuEntry.title(), menuEntry.path());
}
}
private Component createUserMenu() {
// TODO Replace with real user information and actions
var avatar = new Avatar("John Smith");
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(5);
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add("John Smith");
userMenuItem.getSubMenu().addItem("View Profile").setEnabled(false);
userMenuItem.getSubMenu().addItem("Manage Settings").setEnabled(false);
userMenuItem.getSubMenu().addItem("Logout").setEnabled(false);
return userMenu;
}
}

View File

@@ -0,0 +1,33 @@
package de.assecutor.emulatorstation.base.ui.view;
import de.assecutor.emulatorstation.base.ui.component.ViewToolbar;
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 jakarta.annotation.security.PermitAll;
/**
* 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 {
// TODO Replace with your own main view.
MainView() {
addClassName(LumoUtility.Padding.MEDIUM);
add(new ViewToolbar("Main"));
add(new Div("Please select a view from the menu on the left."));
}
/**
* Navigates to the main view.
*/
public static void showMainView() {
UI.getCurrent().navigate(MainView.class);
}
}

View File

@@ -0,0 +1,7 @@
/**
* This package contains reusable or cross-cutting view-related classes.
*/
@NullMarked
package de.assecutor.emulatorstation.base.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,61 @@
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<Long> {
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;
}
}

View File

@@ -0,0 +1,12 @@
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<Task, Long>, JpaSpecificationExecutor<Task> {
// If you don't need a total row count, Slice is better than Page.
Slice<Task> findAllBy(Pageable pageable);
}

View File

@@ -0,0 +1,14 @@
/**
* This package contains the domain model of the Task Management sample feature.
* <p>
* 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.
* </p>
* <p>
* If you have domain classes that are re-usable across multiple features, add them to the {@code base.domain} package.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.domain;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,15 @@
/**
* 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.
* <p>
* 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").
* </p>
* <p>
* If your application is very small, you may not need dedicated feature packages. In that case, move the subpackages
* directly to the application package.
* </p>
*/
package de.assecutor.emulatorstation.taskmanagement;
// TODO Remove this package once you have added real features

View File

@@ -0,0 +1,43 @@
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<Task> list(Pageable pageable) {
return taskRepository.findAllBy(pageable).toList();
}
}

View File

@@ -0,0 +1,11 @@
/**
* This package contains the application services of the Task Management sample feature.
* <p>
* 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.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.service;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,85 @@
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;
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.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<Task> taskGrid;
public TaskListView(TaskService taskService, Clock clock) {
this.taskService = taskService;
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();
dueDate.setPlaceholder("Due date");
dueDate.setAriaLabel("Due date");
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);
}
}

View File

@@ -0,0 +1,14 @@
/**
* This package contains the views of the Task Management sample feature.
* <p>
* You can add as many views as you want to this package, as long as they belong to the same feature.
* </p>
* <p>
* 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.
* </p>
*/
@NullMarked
package de.assecutor.emulatorstation.taskmanagement.ui.view;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,14 @@
server.port=${PORT:8080}
logging.level.org.atmosphere=warn
# Launch the default browser when starting the application in development mode
vaadin.launch-browser=true
# To improve the performance during development.
# For more information https://vaadin.com/docs/latest/flow/integrations/spring/configuration#special-configuration-parameters
vaadin.allowed-packages=com.vaadin,org.vaadin,com.flowingcode,de.assecutor.emulatorstation
# Open-in-view is only needed if you use lazy-loaded entities in your Flow views.
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

View File

@@ -0,0 +1,55 @@
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);
}
}

View File

@@ -0,0 +1,14 @@
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);
}
}

View File

@@ -0,0 +1,10 @@
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.
}

View File

@@ -0,0 +1,57 @@
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);
}
}