diff --git a/src/main/java/mops/gruppen2/domain/event/Event.java b/src/main/java/mops/gruppen2/domain/event/Event.java index 15d6680..981e494 100644 --- a/src/main/java/mops/gruppen2/domain/event/Event.java +++ b/src/main/java/mops/gruppen2/domain/event/Event.java @@ -78,7 +78,7 @@ public abstract class Event { checkGroupIdMatch(group.getId()); applyEvent(group); - updateCache(cache, group); + updateCache(cache, group); // Update erst nachdem apply keine exception geworfen hat // Danach hat die Gruppe nur Nullfelder if (this instanceof DestroyGroupEvent) { diff --git a/src/main/java/mops/gruppen2/domain/event/SetParentEvent.java b/src/main/java/mops/gruppen2/domain/event/SetParentEvent.java index 2de2b60..be1dda9 100644 --- a/src/main/java/mops/gruppen2/domain/event/SetParentEvent.java +++ b/src/main/java/mops/gruppen2/domain/event/SetParentEvent.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; import lombok.extern.log4j.Log4j2; +import mops.gruppen2.domain.exception.BadArgumentException; import mops.gruppen2.domain.exception.NoAccessException; import mops.gruppen2.domain.model.group.Group; import mops.gruppen2.domain.model.group.wrapper.Parent; @@ -29,7 +30,7 @@ public class SetParentEvent extends Event { protected void updateCache(GroupCache cache, Group group) {} @Override - protected void applyEvent(Group group) throws NoAccessException { + protected void applyEvent(Group group) throws NoAccessException, BadArgumentException { group.setParent(exec, parent); log.trace("\t\t\t\t\tNeues Parent: {}", group.getParent()); diff --git a/src/main/java/mops/gruppen2/domain/model/group/Group.java b/src/main/java/mops/gruppen2/domain/model/group/Group.java index f06a0b0..860b2a0 100644 --- a/src/main/java/mops/gruppen2/domain/model/group/Group.java +++ b/src/main/java/mops/gruppen2/domain/model/group/Group.java @@ -278,14 +278,20 @@ public class Group { this.limit = limit; } - public void setParent(String exec, @Valid Parent parent) throws NoAccessException { + public void setParent(String exec, @Valid Parent parent) throws NoAccessException, BadArgumentException { ValidationHelper.throwIfNoAdmin(this, exec); + if (parent.getValue().equals(groupid)) { + throw new BadArgumentException("Die Gruppe kann nicht zu sich selbst gehören!"); + } this.parent = parent; } public void setLink(String exec, @Valid Link link) throws NoAccessException { ValidationHelper.throwIfNoAdmin(this, exec); + if (link.getValue().equals(groupid.toString())) { + throw new BadArgumentException("Link kann nicht der GruppenID entsprechen."); + } this.link = link; } diff --git a/src/main/java/mops/gruppen2/domain/model/group/User.java b/src/main/java/mops/gruppen2/domain/model/group/User.java index 5fa6802..7f781ca 100644 --- a/src/main/java/mops/gruppen2/domain/model/group/User.java +++ b/src/main/java/mops/gruppen2/domain/model/group/User.java @@ -12,6 +12,7 @@ import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; @Log4j2 @Value +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @AllArgsConstructor public class User { diff --git a/src/main/java/mops/gruppen2/domain/model/group/wrapper/Link.java b/src/main/java/mops/gruppen2/domain/model/group/wrapper/Link.java index a79db9a..963dd94 100644 --- a/src/main/java/mops/gruppen2/domain/model/group/wrapper/Link.java +++ b/src/main/java/mops/gruppen2/domain/model/group/wrapper/Link.java @@ -4,18 +4,24 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import java.util.UUID; @Value public class Link { @NotNull - @Size(min = 36, max = 36) @JsonProperty("value") - String value; + UUID value; + + public Link(String value) { + this.value = UUID.fromString(value); + } public static Link RANDOM() { return new Link(UUID.randomUUID().toString()); } + + public String getValue() { + return value.toString(); + } } diff --git a/src/main/java/mops/gruppen2/domain/service/GroupService.java b/src/main/java/mops/gruppen2/domain/service/GroupService.java index 1522394..9569a67 100644 --- a/src/main/java/mops/gruppen2/domain/service/GroupService.java +++ b/src/main/java/mops/gruppen2/domain/service/GroupService.java @@ -24,12 +24,15 @@ import mops.gruppen2.domain.model.group.wrapper.Limit; import mops.gruppen2.domain.model.group.wrapper.Link; import mops.gruppen2.domain.model.group.wrapper.Parent; import mops.gruppen2.domain.model.group.wrapper.Title; +import mops.gruppen2.domain.service.helper.ValidationHelper; import mops.gruppen2.infrastructure.GroupCache; import org.springframework.stereotype.Service; +import javax.validation.Valid; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; /** * Behandelt Aufgaben, welche sich auf eine Gruppe beziehen. @@ -94,9 +97,11 @@ public class GroupService { * @param exec Ausführender User */ public void addUsersToGroup(Group group, String exec, List newUsers) { - setLimit(group, exec, getAdjustedUserLimit(newUsers, group)); + List users = newUsers.stream().distinct().collect(Collectors.toUnmodifiableList()); - newUsers.forEach(newUser -> addUserSilent(group, exec, newUser.getId(), newUser)); + setLimit(group, exec, getAdjustedUserLimit(users, group)); + + users.forEach(newUser -> addUserSilent(group, exec, newUser.getId(), newUser)); } /** @@ -123,6 +128,8 @@ public class GroupService { * @throws EventException Falls der User nicht gefunden wird */ public void toggleMemberRole(Group group, String exec, String target) { + ValidationHelper.throwIfNoMember(group, target); + updateRole(group, exec, target, group.getRole(target).toggle()); } @@ -192,7 +199,7 @@ public class GroupService { * Prüft, ob der Nutzer Admin ist und ob der Titel valide ist. * Bei keiner Änderung wird nichts erzeugt. */ - public void setTitle(Group group, String exec, Title title) { + public void setTitle(Group group, String exec, @Valid Title title) { if (group.getTitle().equals(title.getValue())) { return; } @@ -205,7 +212,7 @@ public class GroupService { * Prüft, ob der Nutzer Admin ist und ob die Beschreibung valide ist. * Bei keiner Änderung wird nichts erzeugt. */ - public void setDescription(Group group, String exec, Description description) { + public void setDescription(Group group, String exec, @Valid Description description) { if (group.getDescription().equals(description.getValue())) { return; } @@ -231,7 +238,7 @@ public class GroupService { * Prüft, ob der Nutzer Admin ist und ob das Limit valide ist. * Bei keiner Änderung wird nichts erzeugt. */ - public void setLimit(Group group, String exec, Limit userLimit) { + public void setLimit(Group group, String exec, @Valid Limit userLimit) { if (userLimit.getValue() == group.getLimit()) { return; } @@ -247,7 +254,8 @@ public class GroupService { applyAndSave(group, new SetParentEvent(group.getId(), exec, parent)); } - public void setLink(Group group, String exec, Link link) { + //TODO: UI Link regenerieren button + public void setLink(Group group, String exec, @Valid Link link) { if (group.getLink().equals(link.getValue())) { return; } diff --git a/src/main/java/mops/gruppen2/domain/service/helper/ProjectionHelper.java b/src/main/java/mops/gruppen2/domain/service/helper/ProjectionHelper.java index 022166d..2e686e9 100644 --- a/src/main/java/mops/gruppen2/domain/service/helper/ProjectionHelper.java +++ b/src/main/java/mops/gruppen2/domain/service/helper/ProjectionHelper.java @@ -22,7 +22,6 @@ import java.util.UUID; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ProjectionHelper { - public static List project(List events) { Map groups = new HashMap<>(); diff --git a/src/main/java/mops/gruppen2/infrastructure/GroupCache.java b/src/main/java/mops/gruppen2/infrastructure/GroupCache.java index 736c975..651ab0c 100644 --- a/src/main/java/mops/gruppen2/infrastructure/GroupCache.java +++ b/src/main/java/mops/gruppen2/infrastructure/GroupCache.java @@ -21,6 +21,13 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; +/** + * Cached alle existierenden Gruppen und einige Beziehungen. + * Gruppen können nach Typ angefragt werden, nach ID, nach Link oder nach User. + * Der Cache wird von den Events aktualisiert. + * Beim Aufruf der init() Methode werden alle bisherigen Events projiziert und die Gruppen gespeichert. + * Die Komplette Anwendung verwendet eine Instanz des Caches. + */ @Log4j2 @RequiredArgsConstructor @Component diff --git a/src/test/java/mops/gruppen2/TestHelper.java b/src/test/java/mops/gruppen2/TestHelper.java index 7895df3..ad9968c 100644 --- a/src/test/java/mops/gruppen2/TestHelper.java +++ b/src/test/java/mops/gruppen2/TestHelper.java @@ -1,5 +1,8 @@ package mops.gruppen2; +import mops.gruppen2.domain.event.Event; + +import java.util.List; import java.util.UUID; public final class TestHelper { @@ -12,4 +15,10 @@ public final class TestHelper { return UUID.fromString(string); } + + public static void initEvents(List events) { + for (int i = 1; i <= events.size(); i++) { + events.get(i - 1).init(i); + } + } } diff --git a/src/test/java/mops/gruppen2/domain/service/GroupServiceTest.java b/src/test/java/mops/gruppen2/domain/service/GroupServiceTest.java new file mode 100644 index 0000000..f0f8c7e --- /dev/null +++ b/src/test/java/mops/gruppen2/domain/service/GroupServiceTest.java @@ -0,0 +1,267 @@ +package mops.gruppen2.domain.service; + +import mops.gruppen2.GroupBuilder; +import mops.gruppen2.domain.exception.BadArgumentException; +import mops.gruppen2.domain.exception.GroupFullException; +import mops.gruppen2.domain.exception.LastAdminException; +import mops.gruppen2.domain.exception.NoAccessException; +import mops.gruppen2.domain.exception.UserAlreadyExistsException; +import mops.gruppen2.domain.exception.UserNotFoundException; +import mops.gruppen2.domain.model.group.Group; +import mops.gruppen2.domain.model.group.Type; +import mops.gruppen2.domain.model.group.User; +import mops.gruppen2.domain.model.group.wrapper.Description; +import mops.gruppen2.domain.model.group.wrapper.Limit; +import mops.gruppen2.domain.model.group.wrapper.Link; +import mops.gruppen2.domain.model.group.wrapper.Parent; +import mops.gruppen2.domain.model.group.wrapper.Title; +import mops.gruppen2.infrastructure.GroupCache; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static mops.gruppen2.TestHelper.uuid; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +class GroupServiceTest { + + private GroupService groupService; + + @BeforeEach + void setUp() { + groupService = new GroupService(mock(GroupCache.class), mock(EventStoreService.class)); + } + + @Test + void createGroup() { + Group group = groupService.createGroup("TEST"); + + assertThat(group.getId()).isNotNull(); + assertThat(group.creator()).isEqualTo("TEST"); + } + + @Test + void initGroupMembers() { + Group group = groupService.createGroup("TEST"); + groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1)); + + assertThat(group.getMembers()).containsExactly(new User("TEST")); + assertThat(group.getLimit()).isEqualTo(1); + assertThat(group.isAdmin("TEST")).isTrue(); + } + + @Test + void initGroupMeta() { + Group group = groupService.createGroup("TEST"); + groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1)); + groupService.initGroupMeta(group, "TEST", Type.PUBLIC, Parent.EMPTY()); + + assertThat(group.isPublic()).isTrue(); + assertThat(group.hasParent()).isFalse(); + } + + @Test + void initGroupText() { + Group group = groupService.createGroup("TEST"); + groupService.initGroupMembers(group, "TEST", "TEST", new User("TEST"), new Limit(1)); + groupService.initGroupMeta(group, "TEST", Type.PUBLIC, Parent.EMPTY()); + groupService.initGroupText(group, "TEST", new Title("TITLE"), new Description("DESCR")); + + assertThat(group.getTitle()).isEqualTo("TITLE"); + assertThat(group.getDescription()).isEqualTo("DESCR"); + } + + @Test + void addUsersToGroup() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.addUsersToGroup(group, "TEST", Arrays.asList( + new User("A"), + new User("B"), + new User("C"), + new User("C"))); + + assertThat(group.getLimit()).isEqualTo(4); + assertThat(group.size()).isEqualTo(4); + assertThat(group.getRegulars()).hasSize(3); + assertThat(group.getAdmins()).hasSize(1); + } + + @Test + void toggleMemberRole_lastAdmin_lastUser() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.toggleMemberRole(group, "TEST", "TEST"); + } + + @Test + void toggleMemberRole_lastAdmin() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build(); + + assertThatThrownBy(() -> groupService.toggleMemberRole(group, "TEST", "TEST")) + .isInstanceOf(LastAdminException.class); + } + + @Test + void toggleMemberRole_noMember() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + assertThatThrownBy(() -> groupService.toggleMemberRole(group, "TEST", "PETER")) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + void addMember_newMember() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).build(); + + groupService.addMember(group, "Test", "PETER", new User("PETER")); + + assertThat(group.size()).isEqualTo(2); + assertThat(group.getAdmins()).hasSize(1); + assertThat(group.getRegulars()).hasSize(1); + } + + @Test + void addMember_newMember_groupFull() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + assertThatThrownBy(() -> groupService.addMember(group, "Test", "PETER", new User("PETER"))) + .isInstanceOf(GroupFullException.class); + } + + @Test + void addMember_newMember_userExists() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(3).add("PETER").build(); + + assertThatThrownBy(() -> groupService.addMember(group, "Test", "PETER", new User("PETER"))) + .isInstanceOf(UserAlreadyExistsException.class); + } + + @Test + void kickMember_noMember() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + assertThatThrownBy(() -> groupService.kickMember(group, "TEST", "PETER")) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + void kickMember_lastMember() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.kickMember(group, "TEST", "TEST"); + + assertThat(group.exists()).isFalse(); + } + + @Test + void kickMember_lastAdmin() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build(); + + assertThatThrownBy(() -> groupService.kickMember(group, "TEST", "TEST")) + .isInstanceOf(LastAdminException.class); + } + + @Test + void deleteGroup_noGroup() { + groupService.deleteGroup(Group.EMPTY(), "TEST"); + } + + @Test + void deleteGroup() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build(); + + groupService.deleteGroup(group, "TEST"); + + assertThat(group.exists()).isFalse(); + } + + @Test + void deleteGroup_noAdmin() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build(); + + assertThatThrownBy(() -> groupService.deleteGroup(group, "PETER")) + .isInstanceOf(NoAccessException.class); + assertThat(group.exists()).isTrue(); + } + + @Test + void setTitle() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.setTitle(group, "TEST", new Title("TITLE")); + + assertThat(group.getTitle()).isEqualTo("TITLE"); + } + + @Test + void setDescription() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.setDescription(group, "TEST", new Description("DESCR")); + + assertThat(group.getDescription()).isEqualTo("DESCR"); + } + + @Test + void setLimit_tooLow() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().limit(2).add("PETER").build(); + + assertThatThrownBy(() -> groupService.setLimit(group, "TEST", new Limit(1))) + .isInstanceOf(BadArgumentException.class); + + assertThat(group.getLimit()).isEqualTo(2); + } + + @Test + void setLimit() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.setLimit(group, "TEST", new Limit(8)); + + assertThat(group.getLimit()).isEqualTo(8); + } + + @Test + void setParent_sameGroup() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + assertThatThrownBy(() -> groupService.setParent(group, "TEST", new Parent(uuid(1).toString()))) + .isInstanceOf(BadArgumentException.class); + + assertThat(group.getParent()).isEqualTo(uuid(0)); + assertThat(group.hasParent()).isFalse(); + } + + @Test + void setParent() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.setParent(group, "TEST", new Parent(uuid(2).toString())); + + assertThat(group.getParent()).isEqualTo(uuid(2)); + assertThat(group.hasParent()).isTrue(); + } + + @Test + void setLink_sameAsGroupId() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + assertThatThrownBy(() -> groupService.setLink(group, "TEST", new Link(uuid(1).toString()))) + .isInstanceOf(BadArgumentException.class); + + assertThat(group.getLink()).isNotEqualTo(uuid(1).toString()); + } + + @Test + void setLink() { + Group group = GroupBuilder.get(mock(GroupCache.class), 1).group().testadmin().build(); + + groupService.setLink(group, "TEST", new Link(uuid(2).toString())); + + assertThat(group.getLink()).isEqualTo(uuid(2).toString()); + } +}