diff --git a/model/src/main/java/io/spine/examples/chatspn/package-info.java b/model/src/main/java/io/spine/examples/chatspn/package-info.java new file mode 100644 index 00000000..79996f1f --- /dev/null +++ b/model/src/main/java/io/spine/examples/chatspn/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains basic data types of the ChatSPN application. + */ +@BoundedContext("Chats") +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn; + +import com.google.errorprone.annotations.CheckReturnValue; +import io.spine.core.BoundedContext; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/java/io/spine/examples/chatspn/user/command/package-info.java b/model/src/main/java/io/spine/examples/chatspn/user/command/package-info.java new file mode 100644 index 00000000..12966a11 --- /dev/null +++ b/model/src/main/java/io/spine/examples/chatspn/user/command/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides ChatSPN User commands and common commands interfaces. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.user.command; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/java/io/spine/examples/chatspn/user/event/package-info.java b/model/src/main/java/io/spine/examples/chatspn/user/event/package-info.java new file mode 100644 index 00000000..2942959b --- /dev/null +++ b/model/src/main/java/io/spine/examples/chatspn/user/event/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides ChatSPN User events and common event interfaces. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.user.event; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/java/io/spine/examples/chatspn/user/package-info.java b/model/src/main/java/io/spine/examples/chatspn/user/package-info.java new file mode 100644 index 00000000..a434057b --- /dev/null +++ b/model/src/main/java/io/spine/examples/chatspn/user/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains User data types of the ChatSPN application. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.user; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/java/io/spine/examples/chatspn/user/rejection/package-info.java b/model/src/main/java/io/spine/examples/chatspn/user/rejection/package-info.java new file mode 100644 index 00000000..1e2760aa --- /dev/null +++ b/model/src/main/java/io/spine/examples/chatspn/user/rejection/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides ChatSPN User rejections. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.user.rejection; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/proto/spine_examples/chatspn/identifiers.proto b/model/src/main/proto/spine_examples/chatspn/identifiers.proto new file mode 100644 index 00000000..938f8528 --- /dev/null +++ b/model/src/main/proto/spine_examples/chatspn/identifiers.proto @@ -0,0 +1,40 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +syntax = "proto3"; + +package spine_examples.chatspn; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.chatspn.spine.io"; +option java_package = "io.spine.examples.chatspn"; +option java_outer_classname = "IdentifiersProto"; +option java_multiple_files = true; + +// Identifies a chat in the scope of the application. +message ChatId { + string uuid = 1 [(required) = true]; +} diff --git a/model/src/main/proto/spine_examples/chatspn/user/commands.proto b/model/src/main/proto/spine_examples/chatspn/user/commands.proto index 3bf2b767..d606ccfd 100644 --- a/model/src/main/proto/spine_examples/chatspn/user/commands.proto +++ b/model/src/main/proto/spine_examples/chatspn/user/commands.proto @@ -45,3 +45,23 @@ message RegisterUser { // A name of the user to register. string name = 2 [(required) = true]; } + +// Tells to block user. +message BlockUser { + + // The ID of the user who wants to block. + spine.core.UserId user_who_block = 1; + + // The ID of the user to be blocked. + spine.core.UserId user_to_block = 2 [(required) = true]; +} + +// Tells to unblock user. +message UnblockUser { + + // The ID of the user who wants to unblock. + spine.core.UserId user_who_unblock = 1; + + // The ID of the user to be unblocked. + spine.core.UserId user_to_unblock = 2 [(required) = true]; +} diff --git a/model/src/main/proto/spine_examples/chatspn/user/events.proto b/model/src/main/proto/spine_examples/chatspn/user/events.proto index 659d21c0..7e63f5de 100644 --- a/model/src/main/proto/spine_examples/chatspn/user/events.proto +++ b/model/src/main/proto/spine_examples/chatspn/user/events.proto @@ -45,3 +45,23 @@ message UserRegistered { // A name of the registered user. string name = 2 [(required) = true]; } + +// A user has been blocked. +message UserBlocked { + + // The ID of the user who blocked. + spine.core.UserId blocking_user = 1; + + // The ID of the user who was blocked. + spine.core.UserId blocked_user = 2 [(required) = true]; +} + +// A user has been unblocked. +message UserUnblocked { + + // The ID of the user who unblocked. + spine.core.UserId unblocking_user = 1; + + // The ID of the user who was unblocked. + spine.core.UserId unblocked_user = 2 [(required) = true]; +} diff --git a/model/src/main/proto/spine_examples/chatspn/user/rejections.proto b/model/src/main/proto/spine_examples/chatspn/user/rejections.proto new file mode 100644 index 00000000..c8578cab --- /dev/null +++ b/model/src/main/proto/spine_examples/chatspn/user/rejections.proto @@ -0,0 +1,64 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +syntax = "proto3"; + +package spine_examples.chatspn.user; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.chatspn.spine.io"; +option java_package = "io.spine.examples.chatspn.user.rejection"; + +// Set the value of `java_multiple_files` to `false` to instruct Protobuf Compiler to put all the +// rejection classes in one outer class. +// +// Then Spine Model Compiler for Java would generate `ThrowableMessage` classes for all +// these messages. These classes will be named after the classes of rejection messages. +// Putting rejection message classes under an outer class avoids name clash inside the package. +// +option java_multiple_files = false; + +import "spine/core/user_id.proto"; + +// A user cannot be blocked. +message UserCannotBeBlocked { + + // The ID of the user that could not block. + spine.core.UserId user_who_block = 1; + + // The ID of the user that could not be blocked. + spine.core.UserId user_to_block = 2 [(required) = true]; +} + +// A user cannot be unblocked. +message UserCannotBeUnblocked { + + // The ID of the user that could not unblock. + spine.core.UserId user_who_unblock = 1; + + // The ID of the user that could not be unblocked. + spine.core.UserId user_to_unblock = 2 [(required) = true]; +} diff --git a/model/src/main/proto/spine_examples/chatspn/user/user.proto b/model/src/main/proto/spine_examples/chatspn/user/user.proto index ab6c7f3d..963f12d7 100644 --- a/model/src/main/proto/spine_examples/chatspn/user/user.proto +++ b/model/src/main/proto/spine_examples/chatspn/user/user.proto @@ -36,6 +36,7 @@ option java_multiple_files = true; import "google/protobuf/timestamp.proto"; import "spine/core/user_id.proto"; +import "spine_examples/chatspn/identifiers.proto"; // A user in the chatting system. message User { @@ -52,4 +53,13 @@ message User { // A time of last user activity. google.protobuf.Timestamp when_last_active = 3; + + // The chats in which the user is a member. + repeated ChatId chat = 4; + + // The chats that the user has muted. + repeated ChatId muted_chat = 5; + + // The users that the user has blocked. + repeated spine.core.UserId blocked_user = 6; } diff --git a/model/src/main/proto/spine_examples/chatspn/user/user_blocklist.proto b/model/src/main/proto/spine_examples/chatspn/user/user_blocklist.proto new file mode 100644 index 00000000..e256a92a --- /dev/null +++ b/model/src/main/proto/spine_examples/chatspn/user/user_blocklist.proto @@ -0,0 +1,48 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +syntax = "proto3"; + +package spine_examples.chatspn.user; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.chatspn.spine.io"; +option java_package = "io.spine.examples.chatspn.user"; +option java_outer_classname = "UserBlocklistProto"; +option java_multiple_files = true; + +import "spine/core/user_id.proto"; + +// A user's blocklist view in the chatting system. +message UserBlocklist { + option (entity) = { kind: PROJECTION }; + + // The ID of the user who owned blocklist. + spine.core.UserId id = 1; + + // User's blocklist. + repeated spine.core.UserId blocked_user = 2; +} diff --git a/server/src/main/java/io/spine/examples/chatspn/server/ChatsContext.java b/server/src/main/java/io/spine/examples/chatspn/server/ChatsContext.java index 28bbf36c..f0049fb5 100644 --- a/server/src/main/java/io/spine/examples/chatspn/server/ChatsContext.java +++ b/server/src/main/java/io/spine/examples/chatspn/server/ChatsContext.java @@ -27,6 +27,7 @@ package io.spine.examples.chatspn.server; import io.spine.examples.chatspn.server.user.UserAggregate; +import io.spine.examples.chatspn.server.user.UserBlocklistRepository; import io.spine.examples.chatspn.server.user.UserProfileRepository; import io.spine.server.BoundedContext; import io.spine.server.BoundedContextBuilder; @@ -53,6 +54,7 @@ public static BoundedContextBuilder newBuilder() { return BoundedContext .singleTenant(NAME) .add(DefaultRepository.of(UserAggregate.class)) - .add(new UserProfileRepository()); + .add(new UserProfileRepository()) + .add(new UserBlocklistRepository()); } } diff --git a/server/src/main/java/io/spine/examples/chatspn/server/package-info.java b/server/src/main/java/io/spine/examples/chatspn/server/package-info.java new file mode 100644 index 00000000..1f6be724 --- /dev/null +++ b/server/src/main/java/io/spine/examples/chatspn/server/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides server-side code of the ChatSPN application. + */ +@BoundedContext(ChatsContext.NAME) +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.server; + +import com.google.errorprone.annotations.CheckReturnValue; +import io.spine.core.BoundedContext; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/main/java/io/spine/examples/chatspn/server/user/UserAggregate.java b/server/src/main/java/io/spine/examples/chatspn/server/user/UserAggregate.java index 06c8b6d2..6f7a8100 100644 --- a/server/src/main/java/io/spine/examples/chatspn/server/user/UserAggregate.java +++ b/server/src/main/java/io/spine/examples/chatspn/server/user/UserAggregate.java @@ -26,10 +26,17 @@ package io.spine.examples.chatspn.server.user; +import com.google.common.base.Objects; import io.spine.core.UserId; import io.spine.examples.chatspn.user.User; +import io.spine.examples.chatspn.user.command.BlockUser; import io.spine.examples.chatspn.user.command.RegisterUser; +import io.spine.examples.chatspn.user.command.UnblockUser; +import io.spine.examples.chatspn.user.event.UserBlocked; import io.spine.examples.chatspn.user.event.UserRegistered; +import io.spine.examples.chatspn.user.event.UserUnblocked; +import io.spine.examples.chatspn.user.rejection.UserCannotBeBlocked; +import io.spine.examples.chatspn.user.rejection.UserCannotBeUnblocked; import io.spine.server.aggregate.Aggregate; import io.spine.server.aggregate.Apply; import io.spine.server.command.Assign; @@ -56,4 +63,67 @@ private void event(UserRegistered e) { builder().setId(e.getUser()) .setName(e.getName()); } + + /** + * Handles the command to block a user. + * + * @throws UserCannotBeBlocked + * if user tells to block himself or already blocked user. + */ + @Assign + UserBlocked handle(BlockUser c) throws UserCannotBeBlocked { + if (Objects.equal(c.getUserToBlock(), c.getUserWhoBlock()) || + state().getBlockedUserList() + .contains(c.getUserToBlock())) { + throw UserCannotBeBlocked + .newBuilder() + .setUserWhoBlock(c.getUserWhoBlock()) + .setUserToBlock(c.getUserToBlock()) + .build(); + } + + return UserBlocked + .newBuilder() + .setBlockingUser(c.getUserWhoBlock()) + .setBlockedUser(c.getUserToBlock()) + .vBuild(); + } + + @Apply + private void event(UserBlocked e) { + builder().addBlockedUser(e.getBlockedUser()); + } + + /** + * Handles the command to unblock a user. + * + * @throws UserCannotBeUnblocked + * if user tells to unblock a non-blocked user. + */ + @Assign + UserUnblocked handle(UnblockUser c) throws UserCannotBeUnblocked { + if (!state().getBlockedUserList() + .contains(c.getUserToUnblock())) { + throw UserCannotBeUnblocked + .newBuilder() + .setUserWhoUnblock(c.getUserWhoUnblock()) + .setUserToUnblock(c.getUserToUnblock()) + .build(); + } + + return UserUnblocked + .newBuilder() + .setUnblockingUser(c.getUserWhoUnblock()) + .setUnblockedUser(c.getUserToUnblock()) + .vBuild(); + } + + @Apply + private void event(UserUnblocked e) { + int unblockedUserIndex = state() + .getBlockedUserList() + .indexOf(e.getUnblockedUser()); + + builder().removeBlockedUser(unblockedUserIndex); + } } diff --git a/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistProjection.java b/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistProjection.java new file mode 100644 index 00000000..98e0cbfa --- /dev/null +++ b/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistProjection.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.chatspn.server.user; + +import io.spine.core.Subscribe; +import io.spine.core.UserId; +import io.spine.examples.chatspn.user.UserBlocklist; +import io.spine.examples.chatspn.user.event.UserBlocked; +import io.spine.examples.chatspn.user.event.UserRegistered; +import io.spine.examples.chatspn.user.event.UserUnblocked; +import io.spine.server.projection.Projection; + +/** + * Manages instances of {@code UserBlocklist} projections. + */ +public final class UserBlocklistProjection extends Projection { + + @Subscribe + void on(UserRegistered e) { + builder().setId(e.getUser()); + } + + @Subscribe + void on(UserBlocked e) { + builder().addBlockedUser(e.getBlockedUser()); + } + + @Subscribe + void on(UserUnblocked e) { + int unblockedUserIndex = state() + .getBlockedUserList() + .indexOf(e.getUnblockedUser()); + + builder().removeBlockedUser(unblockedUserIndex); + } +} diff --git a/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistRepository.java b/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistRepository.java new file mode 100644 index 00000000..d6113c8a --- /dev/null +++ b/server/src/main/java/io/spine/examples/chatspn/server/user/UserBlocklistRepository.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.chatspn.server.user; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.core.UserId; +import io.spine.examples.chatspn.user.UserBlocklist; +import io.spine.examples.chatspn.user.event.UserBlocked; +import io.spine.examples.chatspn.user.event.UserRegistered; +import io.spine.examples.chatspn.user.event.UserUnblocked; +import io.spine.server.projection.ProjectionRepository; +import io.spine.server.route.EventRouting; + +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for managing {@link UserBlocklistProjection} instances. + */ +public final class UserBlocklistRepository + extends ProjectionRepository { + + @OverridingMethodsMustInvokeSuper + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(UserRegistered.class, (event, context) -> withId(event.getUser())) + .route(UserBlocked.class, (event, context) -> withId(event.getBlockingUser())) + .route(UserUnblocked.class, (event, context) -> withId(event.getUnblockingUser())); + } +} diff --git a/server/src/main/java/io/spine/examples/chatspn/server/user/package-info.java b/server/src/main/java/io/spine/examples/chatspn/server/user/package-info.java new file mode 100644 index 00000000..fdf84811 --- /dev/null +++ b/server/src/main/java/io/spine/examples/chatspn/server/user/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Provides server-side classes for working with User. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.examples.chatspn.server.user; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/test/java/io/spine/examples/chatspn/server/given/UserTestEnv.java b/server/src/test/java/io/spine/examples/chatspn/server/given/UserTestEnv.java new file mode 100644 index 00000000..e823e9b5 --- /dev/null +++ b/server/src/test/java/io/spine/examples/chatspn/server/given/UserTestEnv.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.chatspn.server.given; + +import io.spine.examples.chatspn.user.User; +import io.spine.examples.chatspn.user.command.RegisterUser; +import io.spine.testing.core.given.GivenUserId; +import io.spine.testing.server.blackbox.BlackBoxContext; + +import static io.spine.testing.TestValues.randomString; + +public final class UserTestEnv { + + /** + * Prevents instantiation of this test environment. + */ + private UserTestEnv() { + } + + public static User registerRandomUser(BlackBoxContext context) { + User user = User + .newBuilder() + .setId(GivenUserId.generated()) + .setName(randomString()) + .vBuild(); + + RegisterUser command = RegisterUser + .newBuilder() + .setUser(user.getId()) + .setName(user.getName()) + .vBuild(); + + context.receivesCommand(command); + return user; + } +} diff --git a/server/src/test/java/io/spine/examples/chatspn/server/user/UserBlocklistProjectionTest.java b/server/src/test/java/io/spine/examples/chatspn/server/user/UserBlocklistProjectionTest.java new file mode 100644 index 00000000..edac5583 --- /dev/null +++ b/server/src/test/java/io/spine/examples/chatspn/server/user/UserBlocklistProjectionTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.chatspn.server.user; + +import io.spine.examples.chatspn.server.ChatsContext; +import io.spine.examples.chatspn.user.User; +import io.spine.examples.chatspn.user.UserBlocklist; +import io.spine.examples.chatspn.user.command.BlockUser; +import io.spine.examples.chatspn.user.command.UnblockUser; +import io.spine.server.BoundedContextBuilder; +import io.spine.testing.server.blackbox.ContextAwareTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.spine.examples.chatspn.server.given.UserTestEnv.registerRandomUser; + +@DisplayName("`UserBlocklistProjection` should") +class UserBlocklistProjectionTest extends ContextAwareTest { + + @Override + protected BoundedContextBuilder contextBuilder() { + return ChatsContext.newBuilder(); + } + + @Test + @DisplayName("display `UserBlocklist`, as soon as `User` registered") + void reactOnRegistration() { + User user = registerRandomUser(context()); + + UserBlocklist expected = UserBlocklist + .newBuilder() + .setId(user.getId()) + .vBuild(); + + context().assertState(user.getId(), expected); + } + + @Test + @DisplayName("update `UserBlocklist`, as soon as the user blocked") + void reactOnBlocking() { + User blockingUser = registerRandomUser(context()); + User userToBlock = registerRandomUser(context()); + + BlockUser blockingCommand = BlockUser + .newBuilder() + .setUserWhoBlock(blockingUser.getId()) + .setUserToBlock(userToBlock.getId()) + .vBuild(); + + context().receivesCommand(blockingCommand); + + UserBlocklist expected = UserBlocklist + .newBuilder() + .setId(blockingUser.getId()) + .addBlockedUser(userToBlock.getId()) + .vBuild(); + + context().assertState(blockingUser.getId(), expected); + } + + @Test + @DisplayName("update `UserBlocklist`, as soon as the user unblocked") + void reactOnUnblocking() { + User unblockingUser = registerRandomUser(context()); + User userToUnblock = registerRandomUser(context()); + + BlockUser blockingCommand = BlockUser + .newBuilder() + .setUserWhoBlock(unblockingUser.getId()) + .setUserToBlock(userToUnblock.getId()) + .vBuild(); + + context().receivesCommand(blockingCommand); + + UnblockUser unblockingCommand = UnblockUser + .newBuilder() + .setUserWhoUnblock(unblockingUser.getId()) + .setUserToUnblock(userToUnblock.getId()) + .vBuild(); + + context().receivesCommand(unblockingCommand); + + UserBlocklist expected = UserBlocklist + .newBuilder() + .setId(unblockingUser.getId()) + .vBuild(); + + context().assertState(unblockingUser.getId(), expected); + } +} diff --git a/server/src/test/java/io/spine/examples/chatspn/server/user/UserTest.java b/server/src/test/java/io/spine/examples/chatspn/server/user/UserTest.java index 329ed48a..c7b2562d 100644 --- a/server/src/test/java/io/spine/examples/chatspn/server/user/UserTest.java +++ b/server/src/test/java/io/spine/examples/chatspn/server/user/UserTest.java @@ -28,15 +28,19 @@ import io.spine.examples.chatspn.server.ChatsContext; import io.spine.examples.chatspn.user.User; -import io.spine.examples.chatspn.user.command.RegisterUser; +import io.spine.examples.chatspn.user.command.BlockUser; +import io.spine.examples.chatspn.user.command.UnblockUser; +import io.spine.examples.chatspn.user.event.UserBlocked; import io.spine.examples.chatspn.user.event.UserRegistered; +import io.spine.examples.chatspn.user.event.UserUnblocked; +import io.spine.examples.chatspn.user.rejection.Rejections.UserCannotBeBlocked; +import io.spine.examples.chatspn.user.rejection.Rejections.UserCannotBeUnblocked; import io.spine.server.BoundedContextBuilder; -import io.spine.testing.core.given.GivenUserId; import io.spine.testing.server.blackbox.ContextAwareTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static io.spine.testing.TestValues.randomString; +import static io.spine.examples.chatspn.server.given.UserTestEnv.registerRandomUser; @DisplayName("`User` should") class UserTest extends ContextAwareTest { @@ -49,29 +53,174 @@ protected BoundedContextBuilder contextBuilder() { @Test @DisplayName("allow registration and emit the `UserRegistered` event") void registration() { - RegisterUser command = RegisterUser + User user = registerRandomUser(context()); + + UserRegistered expectedEvent = UserRegistered + .newBuilder() + .setUser(user.getId()) + .setName(user.getName()) + .build(); + User expectedState = User + .newBuilder() + .setId(user.getId()) + .setName(user.getName()) + .vBuild(); + + context().assertEvents() + .withType(UserRegistered.class) + .hasSize(1); + context().assertEvent(expectedEvent); + context().assertState(user.getId(), expectedState); + } + + @Test + @DisplayName("allow blocking another user and emit the `UserBlocked` event") + void blocking() { + User blockingUser = registerRandomUser(context()); + User userToBlock = registerRandomUser(context()); + + BlockUser blockingCommand = BlockUser .newBuilder() - .setUser(GivenUserId.generated()) - .setName(randomString()) + .setUserWhoBlock(blockingUser.getId()) + .setUserToBlock(userToBlock.getId()) + .vBuild(); + + context().receivesCommand(blockingCommand); + + UserBlocked expectedEvent = UserBlocked + .newBuilder() + .setBlockingUser(blockingUser.getId()) + .setBlockedUser(userToBlock.getId()) + .build(); + User expectedState = User + .newBuilder() + .setId(blockingUser.getId()) + .setName(blockingUser.getName()) + .addBlockedUser(userToBlock.getId()) + .vBuild(); + + context().assertEvents() + .withType(UserBlocked.class) + .hasSize(1); + context().assertEvent(expectedEvent); + context().assertState(blockingCommand.getUserWhoBlock(), expectedState); + } + + @Test + @DisplayName("reject blocking himself") + void rejectSelfBlocking() { + User user = registerRandomUser(context()); + + BlockUser command = BlockUser + .newBuilder() + .setUserWhoBlock(user.getId()) + .setUserToBlock(user.getId()) .vBuild(); context().receivesCommand(command); - UserRegistered expectedEvent = UserRegistered + UserCannotBeBlocked expectedRejection = UserCannotBeBlocked .newBuilder() - .setUser(command.getUser()) - .setName(command.getName()) + .setUserWhoBlock(user.getId()) + .setUserToBlock(user.getId()) + .vBuild(); + + context().assertEvents() + .withType(UserCannotBeBlocked.class) + .message(0) + .isEqualTo(expectedRejection); + } + + @Test + @DisplayName("reject blocking already blocked user") + void rejectReblocking() { + User blockingUser = registerRandomUser(context()); + User userToBlock = registerRandomUser(context()); + + BlockUser command = BlockUser + .newBuilder() + .setUserWhoBlock(blockingUser.getId()) + .setUserToBlock(userToBlock.getId()) + .vBuild(); + + context().receivesCommand(command); + context().receivesCommand(command); + + UserCannotBeBlocked expectedRejection = UserCannotBeBlocked + .newBuilder() + .setUserWhoBlock(blockingUser.getId()) + .setUserToBlock(userToBlock.getId()) + .vBuild(); + + context().assertEvents() + .withType(UserCannotBeBlocked.class) + .message(0) + .isEqualTo(expectedRejection); + } + + @Test + @DisplayName("allow unblocking blocked user and emit the `UserUnblocked` event") + void unblocking() { + User unblockingUser = registerRandomUser(context()); + User userToUnblock = registerRandomUser(context()); + + BlockUser blockingCommand = BlockUser + .newBuilder() + .setUserWhoBlock(unblockingUser.getId()) + .setUserToBlock(userToUnblock.getId()) + .vBuild(); + + context().receivesCommand(blockingCommand); + + UnblockUser unblockingCommand = UnblockUser + .newBuilder() + .setUserWhoUnblock(unblockingUser.getId()) + .setUserToUnblock(userToUnblock.getId()) + .vBuild(); + + context().receivesCommand(unblockingCommand); + + UserUnblocked expectedEvent = UserUnblocked + .newBuilder() + .setUnblockingUser(unblockingUser.getId()) + .setUnblockedUser(userToUnblock.getId()) .build(); User expectedState = User .newBuilder() - .setId(command.getUser()) - .setName(command.getName()) + .setId(unblockingUser.getId()) + .setName(unblockingUser.getName()) .vBuild(); context().assertEvents() - .withType(UserRegistered.class) + .withType(UserUnblocked.class) .hasSize(1); context().assertEvent(expectedEvent); - context().assertState(command.getUser(), expectedState); + context().assertState(unblockingCommand.getUserWhoUnblock(), expectedState); + } + + @Test + @DisplayName("reject unblocking a non-blocked user") + void rejectUnblockingNonblocked() { + User unblockingUser = registerRandomUser(context()); + User userToUnblock = registerRandomUser(context()); + + UnblockUser unblockingCommand = UnblockUser + .newBuilder() + .setUserWhoUnblock(unblockingUser.getId()) + .setUserToUnblock(userToUnblock.getId()) + .vBuild(); + + context().receivesCommand(unblockingCommand); + + UserCannotBeUnblocked expectedRejection = UserCannotBeUnblocked + .newBuilder() + .setUserWhoUnblock(unblockingUser.getId()) + .setUserToUnblock(userToUnblock.getId()) + .build(); + + context().assertEvents() + .withType(UserCannotBeUnblocked.class) + .message(0) + .isEqualTo(expectedRejection); } }