JavaRush /Java Blog /Random EN /We remove the subscription to articles from the group - "...

We remove the subscription to articles from the group - "Java project from A to Z"

Published in the Random EN group
Hello everyone, my dear friends, future Senior Software Engineers. We continue to develop a telegram bot. In this step of our project, we will consider three tasks that have more visible value than program value. We need to learn how to remove a subscription to new articles from a specific group: use the /stop command to deactivate the bot, and use the /start command to activate it. Moreover, all requests and updates concern only active users of the bot. As usual, we'll update the main branch to get all the changes and create a new one: STEP_7_JRTB-7. In this part, we’ll talk about deleting a subscription and consider 5 options for events - it will be interesting.

JRTB-7: removing a subscription to new articles from a group

It is clear that all users will want to be able to delete their subscription so as not to receive notifications about new articles. Its logic will be very similar to the logic of adding a subscription. If we send only one command, in response we will receive a list of groups and their IDs to which the user is already subscribed, so that we can understand what exactly needs to be deleted. And if the user sends the group ID along with the team, we will delete the subscription. Therefore, let's go develop this command from the telegram bot side.
  1. Let's add the name of the new command - /deleteGroupSub , and in CommandName - the line:

    DELETE_GROUP_SUB("/deleteGroupSub")

  2. Next, let's create the DeleteGroupSubCommand command :

    package com.github.javarushcommunity.jrtb.command;
    
    import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
    import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
    import com.github.javarushcommunity.jrtb.service.GroupSubService;
    import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
    import com.github.javarushcommunity.jrtb.service.TelegramUserService;
    import org.springframework.util.CollectionUtils;
    import org.telegram.telegrambots.meta.api.objects.Update;
    
    import javax.ws.rs.NotFoundException;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.Collectors;
    
    import static com.github.javarushcommunity.jrtb.command.CommandName.DELETE_GROUP_SUB;
    import static com.github.javarushcommunity.jrtb.command.CommandUtils.getChatId;
    import static com.github.javarushcommunity.jrtb.command.CommandUtils.getMessage;
    import static java.lang.String.format;
    import static org.apache.commons.lang3.StringUtils.SPACE;
    import static org.apache.commons.lang3.StringUtils.isNumeric;
    
    /**
    * Delete Group subscription {@link Command}.
    */
    public class DeleteGroupSubCommand implements Command {
    
       private final SendBotMessageService sendBotMessageService;
       private final TelegramUserService telegramUserService;
       private final GroupSubService groupSubService;
    
       public DeleteGroupSubCommand(SendBotMessageService sendBotMessageService, GroupSubService groupSubService,
                                    TelegramUserService telegramUserService) {
           this.sendBotMessageService = sendBotMessageService;
           this.groupSubService = groupSubService;
           this.telegramUserService = telegramUserService;
       }
    
       @Override
       public void execute(Update update) {
           if (getMessage(update).equalsIgnoreCase(DELETE_GROUP_SUB.getCommandName())) {
               sendGroupIdList(getChatId(update));
               return;
           }
           String groupId = getMessage(update).split(SPACE)[1];
           String chatId = getChatId(update);
           if (isNumeric(groupId)) {
               Optional<GroupSub> optionalGroupSub = groupSubService.findById(Integer.valueOf(groupId));
               if (optionalGroupSub.isPresent()) {
                   GroupSub groupSub = optionalGroupSub.get();
                   TelegramUser telegramUser = telegramUserService.findByChatId(chatId).orElseThrow(NotFoundException::new);
                   groupSub.getUsers().remove(telegramUser);
                   groupSubService.save(groupSub);
                   sendBotMessageService.sendMessage(chatId, format("Удалил подписку на группу: %s", groupSub.getTitle()));
               } else {
                   sendBotMessageService.sendMessage(chatId, "Не нашел такой группы =/");
               }
           } else {
               sendBotMessageService.sendMessage(chatId, "неправильный формат ID группы.\n " +
                       "ID должно быть целым положительным числом");
           }
       }
    
       private void sendGroupIdList(String chatId) {
           String message;
           List<GroupSub> groupSubs = telegramUserService.findByChatId(chatId)
                   .orElseThrow(NotFoundException::new)
                   .getGroupSubs();
           if (CollectionUtils.isEmpty(groupSubs)) {
               message = "Пока нет подписок на группы. Whatбы добавить подписку напиши /addGroupSub";
           } else {
               message = "Whatбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
                       "Например: /deleteGroupSub 16 \n\n" +
                       "я подготовил список всех групп, на которые ты подписан) \n\n" +
                       "Name группы - ID группы \n\n" +
                       "%s";
    
           }
           String userGroupSubData = groupSubs.stream()
                   .map(group -> format("%s - %s \n", group.getTitle(), group.getId()))
                   .collect(Collectors.joining());
    
           sendBotMessageService.sendMessage(chatId, format(message, userGroupSubData));
       }
    }

To do this, we had to add two more methods for working with the GroupSub entity - retrieving from the database by ID and saving the entity itself. All these methods simply call ready-made repository methods. I’ll tell you separately about deleting a subscription. In the database schema, this is the table that is responsible for the many-to-many process, and to delete this relationship, you need to delete the record in it. This is if we use the general understanding on the part of the database. But we use Spring Data and there is Hibernate by default, which can do this differently. We get the GroupSub entity, to which all users associated with it will be drawn. From this collection of users we will remove the one we need and save groupSub back into the database, but without this user. This way Spring Data will understand what we wanted and delete the record. "Java project from A to Z": Removing the subscription to articles from the group - 1"Java project from A to Z": Removing the subscription to articles from the group - 2To quickly remove a user, I added the EqualsAndHashCode annotation for TelegramUser, excluding the GroupSub list so that there would be no problems. And called the remove method on the collection of users with the user we need. This is what it looks like for TelegramUser:
@Data
@Entity
@Table(name = "tg_user")
@EqualsAndHashCode(exclude = "groupSubs")
public class TelegramUser {

   @Id
   @Column(name = "chat_id")
   private String chatId;

   @Column(name = "active")
   private boolean active;

   @ManyToMany(mappedBy = "users", fetch = FetchType.EAGER)
   private List<GroupSub> groupSubs;
}
As a result, everything took off just as we wanted. There are several possible events in a team, so writing a good test for each of them is a great idea. Speaking of tests: while I was writing them, I found a defect in the logic and corrected it before it was released into production. If there had been no test, it is unclear how quickly it would have been detected. DeleteGroupSubCommandTest:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.GroupSubService;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.ArrayList;
import java.util.Optional;

import static com.github.javarushcommunity.jrtb.command.AbstractCommandTest.prepareUpdate;
import static com.github.javarushcommunity.jrtb.command.CommandName.DELETE_GROUP_SUB;
import static java.util.Collections.singletonList;

@DisplayName("Unit-level testing for DeleteGroupSubCommand")
class DeleteGroupSubCommandTest {

   private Command command;
   private SendBotMessageService sendBotMessageService;
   GroupSubService groupSubService;
   TelegramUserService telegramUserService;


   @BeforeEach
   public void init() {
       sendBotMessageService = Mockito.mock(SendBotMessageService.class);
       groupSubService = Mockito.mock(GroupSubService.class);
       telegramUserService = Mockito.mock(TelegramUserService.class);

       command = new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService);
   }

   @Test
   public void shouldProperlyReturnEmptySubscriptionList() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, DELETE_GROUP_SUB.getCommandName());

       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(new TelegramUser()));

       String expectedMessage = "Пока нет подписок на группы. Whatбы добавить подписку напиши /addGroupSub";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldProperlyReturnSubscriptionLit() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, DELETE_GROUP_SUB.getCommandName());
       TelegramUser telegramUser = new TelegramUser();
       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       telegramUser.setGroupSubs(singletonList(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "Whatбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
               "Например: /deleteGroupSub 16 \n\n" +
               "я подготовил список всех групп, на которые ты подписан) \n\n" +
               "Name группы - ID группы \n\n" +
               "GS1 Title - 123 \n";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldRejectByInvalidGroupId() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), "groupSubId"));
       TelegramUser telegramUser = new TelegramUser();
       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       telegramUser.setGroupSubs(singletonList(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "неправильный формат ID группы.\n " +
               "ID должно быть целым положительным числом";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldProperlyDeleteByGroupId() {
       //given

       /// prepare update object
       Long chatId = 23456L;
       Integer groupId = 1234;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), groupId));


       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       TelegramUser telegramUser = new TelegramUser();
       telegramUser.setChatId(chatId.toString());
       telegramUser.setGroupSubs(singletonList(gs1));
       ArrayList<TelegramUser> users = new ArrayList<>();
       users.add(telegramUser);
       gs1.setUsers(users);
       Mockito.when(groupSubService.findById(groupId)).thenReturn(Optional.of(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "Удалил подписку на группу: GS1 Title";

       //when
       command.execute(update);

       //then
       users.remove(telegramUser);
       Mockito.verify(groupSubService).save(gs1);
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldDoesNotExistByGroupId() {
       //given
       Long chatId = 23456L;
       Integer groupId = 1234;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), groupId));


       Mockito.when(groupSubService.findById(groupId)).thenReturn(Optional.empty());

       String expectedMessage = "Не нашел такой группы =/";

       //when
       command.execute(update);

       //then
       Mockito.verify(groupSubService).findById(groupId);
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }
}
Here, each test checks a separate scenario, and let me remind you, there are only five of them:
  • when you simply passed the /deleteGroupSub command and there are no group subscriptions;
  • when you simply passed the /deleteGroupSub command and there are subscriptions to groups;
  • when an invalid group ID was passed, for example, /deleteGroupSub abc ;
  • a scenario in which everything is deleted correctly, as expected;
  • a scenario when the group ID is valid, but such a group is not in the database.
As you can see, all these scenarios need to be covered with tests. While I was writing, I realized that in order to write better tests, it’s worth taking some testing courses. I think this will help to properly look for different options. That's right, thoughts for the future. Next, you need to add a description to the /help command that you can now delete the subscription. Let's place it in the section for working with subscriptions. "Java project from A to Z": Removing the subscription to articles from the group - 3Of course, for this command to work, you need to add its initialization to the CommandContainer :
.put(DELETE_GROUP_SUB.getCommandName(),
       new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Now you can test the functionality on a test bot. We launch our database using docker-compose-test.yml: docker-compose -f docker-compose-test.yml up And launch SpringBoot via IDEA. I will completely clear the correspondence with the bot and start again. I’ll run through all the options that may arise when working with this team. "Java project from A to Z": Removing the subscription to articles from the group - 4As you can see from the screenshot, all options went through and were successful.
Friends! Do you want to know immediately when new code for a project is released? When does a new article come out? Join my Telegram channel . There I collect my articles, thoughts and open source development together.
We update the version of our project to 0.6.0-SNAPSHOT We update RELEASE_NOTES.md, adding a description of what has been done in the new version:
## 0.6.0-SNAPSHOT * JRTB-7: added the ability to delete group subscription.
The code works, tests have been written for it: it’s time to push the task into the repository and create a pull request."Java project from A to Z": Removing the subscription to articles from the group - 5

Instead of ending

We haven’t looked at our project board for a long time, but there have been big changes: "Java project from A to Z": Removing a subscription to articles from the group - 6There are only 5 tasks left. That is, you and I are already at the very end of the road. Left a little. It’s especially interesting to note that this series of articles has been running since mid-September, that is, for 7 months!!! When I came up with this idea, I didn’t expect it to take sooo long. At the same time, I am more than pleased with the result! Friends, if it is not clear what is happening in the article, ask questions in the comments. This way I will know that something needs to be better described, and something needs further explanation. Well, as usual, like - subscribe - ring the bell, give our project a star , write comments and rate the article! Thanks to all. Until the next part. We'll talk soon about how to add bot deactivation and activation via /stop & /start commands and how to best use them. See you later!

A list of all materials in the series is at the beginning of this article.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION