JavaRush /Blog Java /Random-ES /Eliminamos la suscripción a artículos del grupo - "Proyec...

Eliminamos la suscripción a artículos del grupo - "Proyecto Java de la A a la Z"

Publicado en el grupo Random-ES
Hola a todos, mis queridos amigos, futuros ingenieros de software senior. Seguimos desarrollando un bot de Telegram. En este paso de nuestro proyecto, consideraremos tres tareas que tienen un valor más visible que el valor del programa. Necesitamos aprender cómo eliminar una suscripción a nuevos artículos de un grupo específico: use el comando /stop para desactivar el bot y use el comando /start para activarlo. Además, todas las solicitudes y actualizaciones conciernen únicamente a los usuarios activos del bot. Como de costumbre, actualizaremos la rama principal para obtener todos los cambios y crear una nueva: STEP_7_JRTB-7. En esta parte, hablaremos sobre cómo eliminar una suscripción y consideraremos 5 opciones para eventos; será interesante.

JRTB-7: eliminar una suscripción a nuevos artículos de un grupo

Está claro que todos los usuarios querrán poder eliminar su suscripción para no recibir notificaciones sobre nuevos artículos. Su lógica será muy similar a la lógica de agregar una suscripción. Si enviamos solo un comando, en respuesta recibiremos una lista de grupos y sus ID a los que el usuario ya está suscrito, para que podamos entender qué es exactamente lo que se debe eliminar. Y si el usuario envía el ID del grupo junto con el equipo, eliminaremos la suscripción. Por lo tanto, desarrollemos este comando desde el lado del bot de Telegram.
  1. Agreguemos el nombre del nuevo comando - /deleteGroupSub , y en CommandName - la línea:

    
    DELETE_GROUP_SUB("/deleteGroupSub")

  2. A continuación, creemos el 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 = "Пока нет подписок на группы. Quéбы добавить подписку напиши /addGroupSub";
           } else {
               message = "Quéбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
                       "Например: /deleteGroupSub 16 \n\n" +
                       "я подготовил список всех групп, на которые ты подписан) \n\n" +
                       "Nombre группы - 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 hacer esto, tuvimos que agregar dos métodos más para trabajar con la entidad GroupSub: recuperar de la base de datos por ID y guardar la entidad misma. Todos estos métodos simplemente llaman a métodos de repositorio ya preparados. Te contaré por separado sobre cómo eliminar una suscripción. En el esquema de la base de datos, esta es la tabla responsable del proceso de muchos a muchos y, para eliminar esta relación, debe eliminar el registro que contiene. Esto es si utilizamos la comprensión general por parte de la base de datos. Pero usamos Spring Data y existe Hibernate de forma predeterminada, que puede hacer esto de manera diferente. Obtenemos la entidad GroupSub, a la que se atraerán todos los usuarios asociados con ella. De esta colección de usuarios eliminaremos el que necesitamos y guardaremos groupSub nuevamente en la base de datos, pero sin este usuario. De esta manera Spring Data comprenderá lo que queríamos y eliminará el registro. "Proyecto Java de la A a la Z": Eliminar la suscripción a artículos del grupo - 1"Proyecto Java de la A a la Z": Eliminar la suscripción a artículos del grupo - 2Para eliminar rápidamente a un usuario, agregué la anotación EqualsAndHashCode para TelegramUser, excluyendo la lista GroupSub para que no hubiera problemas. Y llamó al método de eliminación en la colección de usuarios con el usuario que necesitamos. Así es como se ve 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, todo salió como queríamos. Hay varios eventos posibles en un equipo, por lo que escribir una buena prueba para cada uno de ellos es una gran idea. Hablando de pruebas: mientras las escribía, encontré un defecto en la lógica y lo corregí antes de que fuera lanzado a producción. Si no se hubiera realizado ninguna prueba, no está claro qué tan rápido se habría detectado. EliminarGroupSubCommandTest:

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 = "Пока нет подписок на группы. Quéбы добавить подписку напиши /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 = "Quéбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
               "Например: /deleteGroupSub 16 \n\n" +
               "я подготовил список всех групп, на которые ты подписан) \n\n" +
               "Nombre группы - 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);
   }
}
Aquí, cada prueba verifica un escenario separado y, permítanme recordarles, solo hay cinco:
  • cuando simplemente pasó el comando /deleteGroupSub y no hay suscripciones de grupo;
  • cuando simplemente pasó el comando /deleteGroupSub y hay suscripciones a grupos;
  • cuando se pasó un ID de grupo no válido, por ejemplo, /deleteGroupSub abc ;
  • un escenario en el que todo se elimina correctamente, como se esperaba;
  • un escenario en el que el ID del grupo es válido, pero dicho grupo no está en la base de datos.
Como puede ver, todos estos escenarios deben cubrirse con pruebas. Mientras escribía, me di cuenta de que para escribir mejores pruebas, vale la pena tomar algunos cursos de pruebas. Creo que esto ayudará a buscar adecuadamente diferentes opciones. Así es, pensamientos para el futuro. A continuación, debe agregar una descripción al comando /help para poder eliminar la suscripción. Coloquémoslo en la sección para trabajar con suscripciones. "Proyecto Java de la A a la Z": Eliminar la suscripción a artículos del grupo - 3Por supuesto, para que este comando funcione, es necesario agregar su inicialización al CommandContainer :

.put(DELETE_GROUP_SUB.getCommandName(),
       new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Ahora puedes probar la funcionalidad en un bot de prueba. Lanzamos nuestra base de datos usando docker-compose-test.yml: docker-compose -f docker-compose-test.yml up Y lanzamos SpringBoot a través de IDEA. Borraré por completo la correspondencia con el bot y empezaré de nuevo. Revisaré todas las opciones que puedan surgir al trabajar con este equipo. "Proyecto Java de la A a la Z": Eliminar la suscripción a artículos del grupo - 4Como puede ver en la captura de pantalla, todas las opciones se realizaron y tuvieron éxito.
¡Amigos! ¿Quiere saber inmediatamente cuándo se publica un nuevo código para un proyecto? ¿Cuándo sale un nuevo artículo? Únete a mi canal de Telegram . Allí recopilo mis artículos, pensamientos y desarrollo de código abierto.
Actualizamos la versión de nuestro proyecto a 0.6.0-SNAPSHOT Actualizamos RELEASE_NOTES.md, añadiendo una descripción de lo realizado en la nueva versión:
## 0.6.0-SNAPSHOT * JRTB-7: se agregó la capacidad de eliminar la suscripción grupal.
El código funciona, se han escrito pruebas para él: es hora de enviar la tarea al repositorio y crear una solicitud de extracción."Proyecto Java de la A a la Z": Eliminar la suscripción a artículos del grupo - 5

en lugar de terminar

Hace mucho que no miramos nuestro tablero de proyecto, pero ha habido grandes cambios: "Proyecto Java de la A a la Z": Eliminar una suscripción a artículos del grupo - 6solo quedan 5 tareas. Es decir, tú y yo ya estamos al final del camino. Dejó un poco. Es especialmente interesante notar que esta serie de artículos se publica desde mediados de septiembre, es decir, ¡¡¡desde hace 7 meses!!! Cuando se me ocurrió esta idea, no esperaba que me llevara tanto tiempo. Al mismo tiempo, ¡estoy más que satisfecho con el resultado! Amigos, si no queda claro lo que está pasando en el artículo, hagan preguntas en los comentarios. De esta manera sabré que algo necesita describirse mejor y algo necesita más explicación. Bueno, como siempre, dale me gusta, suscríbete, toca el timbre, dale una estrella a nuestro proyecto , escribe comentarios y califica el artículo. Gracias a todos. Hasta la siguiente parte. Pronto hablaremos sobre cómo agregar la desactivación y activación de bots mediante los comandos /stop & /start y cómo usarlos mejor. ¡Hasta luego!

Al principio de este artículo encontrará una lista de todos los materiales de la serie.

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