JavaRush /Blogue Java /Random-PT /Removemos a assinatura de artigos do grupo - "Projeto Jav...
Roman Beekeeper
Nível 35

Removemos a assinatura de artigos do grupo - "Projeto Java de A a Z"

Publicado no grupo Random-PT
Olá a todos, meus queridos amigos, futuros engenheiros de software sênior. Continuamos a desenvolver um bot de telegrama. Nesta etapa do nosso projeto, consideraremos três tarefas que têm valor mais visível do que o valor do programa. Precisamos aprender como remover a assinatura de novos artigos de um grupo específico: use o comando /stop para desativar o bot e use o comando /start para ativá-lo. Além disso, todas as solicitações e atualizações dizem respeito apenas aos usuários ativos do bot. Como de costume, atualizaremos o branch principal para obter todas as alterações e criaremos um novo: STEP_7_JRTB-7. Nesta parte falaremos sobre a exclusão de uma assinatura e consideraremos 5 opções de eventos - será interessante.

JRTB-7: removendo uma assinatura para novos artigos de um grupo

É claro que todos os usuários desejarão excluir sua assinatura para não receber notificações sobre novos artigos. Sua lógica será muito semelhante à lógica de adicionar uma assinatura. Se enviarmos apenas um comando, em resposta receberemos uma lista de grupos e seus IDs nos quais o usuário já está inscrito, para que possamos entender exatamente o que precisa ser excluído. E se o usuário enviar o ID do grupo junto com a equipe, excluiremos a assinatura. Portanto, vamos desenvolver este comando do lado do bot do telegrama.
  1. Vamos adicionar o nome do novo comando - /deleteGroupSub , e em CommandName - a linha:

    DELETE_GROUP_SUB("/deleteGroupSub")

  2. A seguir, vamos criar o comando DeleteGroupSubCommand :

    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));
       }
    }

Para fazer isso, tivemos que adicionar mais dois métodos para trabalhar com a entidade GroupSub - recuperar do banco de dados por ID e salvar a própria entidade. Todos esses métodos simplesmente chamam métodos de repositório prontos. Falarei separadamente sobre como excluir uma assinatura. No esquema do banco de dados, esta é a tabela responsável pelo processo muitos para muitos e, para excluir esse relacionamento, é necessário excluir o registro nela contido. Isso se usarmos o entendimento geral por parte do banco de dados. Mas usamos Spring Data e existe o Hibernate por padrão, que pode fazer isso de forma diferente. Obtemos a entidade GroupSub, para a qual serão atraídos todos os usuários associados a ela. Desta coleção de usuários iremos remover aquele que precisamos e salvar o groupSub de volta no banco de dados, mas sem este usuário. Dessa forma o Spring Data entenderá o que queríamos e excluirá o registro. "Projeto Java de A a Z": Removendo a assinatura de artigos do grupo - 1"Projeto Java de A a Z": Removendo a assinatura de artigos do grupo - 2Para remover rapidamente um usuário, adicionei a anotação EqualsAndHashCode para TelegramUser, excluindo a lista GroupSub para que não houvesse problemas. E chamamos o método remove na coleção de usuários com o usuário que precisamos. Isto é o que parece para 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;
}
Como resultado, tudo decolou como queríamos. Existem vários eventos possíveis em uma equipe, então escrever um bom teste para cada um deles é uma ótima ideia. Falando em testes: enquanto os escrevia, encontrei um defeito na lógica e corrigi-o antes de colocá-lo em produção. Se não tivesse havido teste, não está claro com que rapidez teria sido detectado. ExcluirGroupSubCommandTest:
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);
   }
}
Aqui, cada teste verifica um cenário separado e, deixe-me lembrá-lo, existem apenas cinco deles:
  • quando você simplesmente passou o comando /deleteGroupSub e não há assinaturas de grupo;
  • quando você simplesmente passou o comando /deleteGroupSub e há assinaturas em grupos;
  • quando um ID de grupo inválido foi passado, por exemplo, /deleteGroupSub abc ;
  • um cenário em que tudo é apagado corretamente, conforme o esperado;
  • um cenário em que o ID do grupo é válido, mas esse grupo não está no banco de dados.
Como você pode ver, todos esses cenários precisam ser cobertos com testes. Enquanto escrevia, percebi que para escrever testes melhores vale a pena fazer alguns cursos de testes. Acho que isso ajudará a procurar adequadamente diferentes opções. Isso mesmo, pensamentos para o futuro. Em seguida, você precisa adicionar uma descrição ao comando /help para que agora você possa excluir a assinatura. Vamos colocá-lo na seção para trabalhar com assinaturas. "Projeto Java de A a Z": Removendo a assinatura de artigos do grupo - 3Claro, para que este comando funcione, você precisa adicionar sua inicialização ao CommandContainer :
.put(DELETE_GROUP_SUB.getCommandName(),
       new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Agora você pode testar a funcionalidade em um bot de teste. Lançamos nosso banco de dados usando docker-compose-test.yml: docker-compose -f docker-compose-test.yml up E lançamos o SpringBoot via IDEA. Vou limpar completamente a correspondência com o bot e começar de novo. Analisarei todas as opções que podem surgir ao trabalhar com esta equipe. "Projeto Java de A a Z": Removendo a assinatura de artigos do grupo - 4Como você pode ver na captura de tela, todas as opções foram executadas e bem-sucedidas.
Amigos! Você quer saber imediatamente quando um novo código para um projeto for lançado? Quando sai um novo artigo? Junte-se ao meu canal Telegram . Lá eu reúno meus artigos, pensamentos e desenvolvimento de código aberto.
Atualizamos a versão do nosso projeto para 0.6.0-SNAPSHOT Atualizamos RELEASE_NOTES.md, adicionando uma descrição do que foi feito na nova versão:
## 0.6.0-SNAPSHOT * JRTB-7: adicionada a capacidade de excluir assinatura de grupo.
O código funciona, testes foram escritos para ele: é hora de enviar a tarefa para o repositório e criar uma solicitação pull."Projeto Java de A a Z": Removendo a assinatura de artigos do grupo - 5

Em vez de terminar

Faz muito tempo que não olhamos para o nosso quadro de projetos, mas houve grandes mudanças: "Projeto Java de A a Z": Removendo assinatura de artigos do grupo - 6restam apenas 5 tarefas. Ou seja, você e eu já estamos no fim do caminho. Deixou um pouco. É especialmente interessante notar que esta série de artigos está no ar desde meados de setembro, ou seja, há 7 meses!!! Quando tive essa ideia, não esperava que demorasse tanto. Ao mesmo tempo, estou mais do que satisfeito com o resultado! Amigos, se não estiver claro o que está acontecendo no artigo, tirem dúvidas nos comentários. Assim saberei que algo precisa ser melhor descrito e algo precisa de mais explicações. Bem, como sempre, curta - inscreva-se - toque a campainha, dê uma estrela ao nosso projeto , escreva comentários e avalie o artigo! Obrigado a todos. Até a próxima parte. Falaremos em breve sobre como adicionar desativação e ativação de bot por meio dos comandos /stop e /start e como usá-los da melhor forma. Até mais!

Uma lista de todos os materiais da série está no início deste artigo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION