Generated project
This commit is contained in:
23
src/main/frontend/index.html
Normal file
23
src/main/frontend/index.html
Normal 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>
|
||||
0
src/main/frontend/themes/default/styles.css
Normal file
0
src/main/frontend/themes/default/styles.css
Normal file
9
src/main/frontend/themes/default/theme.json
Normal file
9
src/main/frontend/themes/default/theme.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"lumoImports": [
|
||||
"typography",
|
||||
"color",
|
||||
"spacing",
|
||||
"badge",
|
||||
"utility"
|
||||
]
|
||||
}
|
||||
24
src/main/java/de/assecutor/votianlt/Application.java
Normal file
24
src/main/java/de/assecutor/votianlt/Application.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package de.assecutor.votianlt;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.assecutor.votianlt.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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.base.domain;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.assecutor.votianlt.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.base.ui.component;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,32 @@
|
||||
package de.assecutor.votianlt.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);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.assecutor.votianlt.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 static com.vaadin.flow.theme.lumo.LumoUtility.*;
|
||||
|
||||
@Layout
|
||||
public final class MainLayout extends AppLayout {
|
||||
|
||||
public 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("Votianlt");
|
||||
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");
|
||||
userMenuItem.getSubMenu().addItem("Manage Settings");
|
||||
userMenuItem.getSubMenu().addItem("Logout");
|
||||
|
||||
return userMenu;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.base.ui.view;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
61
src/main/java/de/assecutor/votianlt/todo/domain/Todo.java
Normal file
61
src/main/java/de/assecutor/votianlt/todo/domain/Todo.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package de.assecutor.votianlt.todo.domain;
|
||||
|
||||
import de.assecutor.votianlt.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 = "todo")
|
||||
public class Todo extends AbstractEntity<Long> {
|
||||
|
||||
public static final int DESCRIPTION_MAX_LENGTH = 255;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "todo_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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.assecutor.votianlt.todo.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 TodoRepository extends JpaRepository<Todo, Long>, JpaSpecificationExecutor<Todo> {
|
||||
|
||||
// If you don't need a total row count, Slice is better than Page.
|
||||
Slice<Todo> findAllBy(Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.todo.domain;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,43 @@
|
||||
package de.assecutor.votianlt.todo.service;
|
||||
|
||||
import de.assecutor.votianlt.todo.domain.Todo;
|
||||
import de.assecutor.votianlt.todo.domain.TodoRepository;
|
||||
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 TodoService {
|
||||
|
||||
private final TodoRepository todoRepository;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
TodoService(TodoRepository todoRepository, Clock clock) {
|
||||
this.todoRepository = todoRepository;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public void createTodo(String description, @Nullable LocalDate dueDate) {
|
||||
if ("fail".equals(description)) {
|
||||
throw new RuntimeException("This is for testing the error handler");
|
||||
}
|
||||
var todo = new Todo();
|
||||
todo.setDescription(description);
|
||||
todo.setCreationDate(clock.instant());
|
||||
todo.setDueDate(dueDate);
|
||||
todoRepository.saveAndFlush(todo);
|
||||
}
|
||||
|
||||
public List<Todo> list(Pageable pageable) {
|
||||
return todoRepository.findAllBy(pageable).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.todo.service;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.assecutor.votianlt.todo.ui.view;
|
||||
|
||||
import de.assecutor.votianlt.base.ui.component.ViewToolbar;
|
||||
import de.assecutor.votianlt.todo.domain.Todo;
|
||||
import de.assecutor.votianlt.todo.service.TodoService;
|
||||
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 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("")
|
||||
@PageTitle("Task List")
|
||||
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List")
|
||||
public class TodoView extends Main {
|
||||
|
||||
private final TodoService todoService;
|
||||
|
||||
final TextField description;
|
||||
final DatePicker dueDate;
|
||||
final Button createBtn;
|
||||
final Grid<Todo> todoGrid;
|
||||
|
||||
public TodoView(TodoService todoService, Clock clock) {
|
||||
this.todoService = todoService;
|
||||
|
||||
description = new TextField();
|
||||
description.setPlaceholder("What do you want to do?");
|
||||
description.setAriaLabel("Task description");
|
||||
description.setMaxLength(Todo.DESCRIPTION_MAX_LENGTH);
|
||||
description.setMinWidth("20em");
|
||||
|
||||
dueDate = new DatePicker();
|
||||
dueDate.setPlaceholder("Due date");
|
||||
dueDate.setAriaLabel("Due date");
|
||||
|
||||
createBtn = new Button("Create", event -> createTodo());
|
||||
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(clock.getZone())
|
||||
.withLocale(getLocale());
|
||||
var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale());
|
||||
|
||||
todoGrid = new Grid<>();
|
||||
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
|
||||
todoGrid.addColumn(Todo::getDescription).setHeader("Description");
|
||||
todoGrid.addColumn(todo -> Optional.ofNullable(todo.getDueDate()).map(dateFormatter::format).orElse("Never"))
|
||||
.setHeader("Due Date");
|
||||
todoGrid.addColumn(todo -> dateTimeFormatter.format(todo.getCreationDate())).setHeader("Creation Date");
|
||||
todoGrid.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(todoGrid);
|
||||
}
|
||||
|
||||
private void createTodo() {
|
||||
todoService.createTodo(description.getValue(), dueDate.getValue());
|
||||
todoGrid.getDataProvider().refreshAll();
|
||||
description.clear();
|
||||
dueDate.clear();
|
||||
Notification.show("Task added", 3000, Notification.Position.BOTTOM_END)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.assecutor.votianlt.todo.ui.view;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
12
src/main/resources/application.properties
Normal file
12
src/main/resources/application.properties
Normal file
@@ -0,0 +1,12 @@
|
||||
server.port=${PORT:8080}
|
||||
logging.level.org.atmosphere=warn
|
||||
spring.mustache.check-template-location=false
|
||||
|
||||
# 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,de.assecutor.votianlt
|
||||
|
||||
# Open-in-view is only needed if you use lazy-loaded entities in your Flow views.
|
||||
spring.jpa.open-in-view=false
|
||||
48
src/test/java/de/assecutor/votianlt/ArchitectureTest.java
Normal file
48
src/test/java/de/assecutor/votianlt/ArchitectureTest.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package de.assecutor.votianlt;
|
||||
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
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;
|
||||
|
||||
@AnalyzeClasses(packages = ArchitectureTest.BASE_PACKAGE)
|
||||
class ArchitectureTest {
|
||||
|
||||
static final String BASE_PACKAGE = "de.assecutor.votianlt";
|
||||
|
||||
// TODO Add your own rules and remove those that don't apply to your project
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule domain_model_should_not_depend_on_application_services = noClasses().that()
|
||||
.resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat()
|
||||
.resideInAPackage(BASE_PACKAGE + "..service..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule domain_model_should_not_depend_on_the_user_interface = noClasses().that()
|
||||
.resideInAPackage(BASE_PACKAGE + "..domain..").should().dependOnClassesThat()
|
||||
.resideInAnyPackage(BASE_PACKAGE + "..ui..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule 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..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule repositories_should_only_be_accessed_by_transactional_classes = classes().that()
|
||||
.areAssignableTo(Repository.class).should().onlyBeAccessed().byClassesThat()
|
||||
.areAnnotatedWith(Transactional.class);
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule application_services_should_not_depend_on_the_user_interface = noClasses().that()
|
||||
.resideInAPackage(BASE_PACKAGE + "..service..").should().dependOnClassesThat()
|
||||
.resideInAnyPackage(BASE_PACKAGE + "..ui..");
|
||||
|
||||
@ArchTest
|
||||
public static final ArchRule there_should_not_be_circular_dependencies_between_feature_packages = slices()
|
||||
.matching(BASE_PACKAGE + ".(*)..").should().beFreeOfCycles();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.assecutor.votianlt.todo.service;
|
||||
|
||||
import de.assecutor.votianlt.todo.domain.Todo;
|
||||
import de.assecutor.votianlt.todo.domain.TodoRepository;
|
||||
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.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;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
class TodoServiceIT {
|
||||
|
||||
@Autowired
|
||||
TodoService todoService;
|
||||
|
||||
@Autowired
|
||||
TodoRepository todoRepository;
|
||||
|
||||
@Autowired
|
||||
Clock clock;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
todoRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void todos_are_stored_in_the_database_with_the_current_timestamp() {
|
||||
var now = clock.instant();
|
||||
var due = LocalDate.of(2025, 2, 7);
|
||||
todoService.createTodo("Do this", due);
|
||||
assertThat(todoService.list(PageRequest.ofSize(1))).singleElement()
|
||||
.matches(todo -> todo.getDescription().equals("Do this") && due.equals(todo.getDueDate())
|
||||
&& todo.getCreationDate().isAfter(now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void todos_are_validated_before_they_are_stored() {
|
||||
assertThatThrownBy(() -> todoService.createTodo("X".repeat(Todo.DESCRIPTION_MAX_LENGTH + 1), null))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
assertThat(todoRepository.count()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user