Всем привет, мои дорогие друзья, будущие Senior Software Engineers. Продолжаем разработку телеграм-бота. На этом шаге нашего проекта рассмотрим три задачи, у которых больше видимого значения, чем программного.
Нам нужно научиться удалять подписку на новые статьи из определенной группы: при помощи команды /stop деактивировать бота, а при помощи команды /start — активировать. Причем так, чтобы все запросы и обновления касались только активных пользователей бота.
Как обычно, обновим main ветку, чтобы получить все изменения, и создадим новую: STEP_7_JRTB-7.
В этой части поговорим об удалении подписки и рассмотрим 5 вариантов событий — будет интересно.
Обновляем версию нашего проекта на 0.6.0-SNAPSHOT
Обновляем RELEASE_NOTES.md, добавляя описание того, что сделано в новой версии:
лайк - подписка - колокольчик, ставьте звезду нашему проекту, пишите комментарии и оценивайте статью!
Всем спасибо. До следующей части. Скоро поговорим о том, как добавить деактивацию и активацию бота через команды /stop & /start и том, как их лучше использовать.
До скорого!
JRTB-7: удаление подписки на новые статьи из группы
Ясное дело, что всем пользователям захочется иметь возможность удалить подписку, чтобы не получать уведомления о новых статьях. Его логика будет очень похожа на логику добавления подписки. Если мы передаем только одну команду, в ответ нам придет список групп и их ID, на которые пользователь уже подписан, чтобы можно было понять, что именно нужно удалить. А если пользователь вместе с командой передаст ID группы, мы удалим подписку. Поэтому пойдем разрабатывать эту команду со стороны телеграм-бота.Добавим имя новой команды — /deleteGroupSub, а в CommandName — строку:
DELETE_GROUP_SUB("/deleteGroupSub")
Далее создадим команду 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 = "Пока нет подписок на группы. Чтобы добавить подписку напиши /addGroupSub"; } else { message = "Чтобы удалить подписку на группу - передай комадну вместе с ID группы. \n" + "Например: /deleteGroupSub 16 \n\n" + "я подготовил список всех групп, на которые ты подписан) \n\n" + "имя группы - 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)); } }
@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;
}
В результате все взлетело, как и мы хотели.
В команде есть несколько вариантов событий, поэтому написать хороший тест на каждый из них — это отличная идея. К слову о тестах: пока писал их, нашел дефект в логике и исправил его еще до выхода в прод. Не было бы теста — неясно, как быстро обнаружил бы его.
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 = "Пока нет подписок на группы. Чтобы добавить подписку напиши /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 = "Чтобы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
"Например: /deleteGroupSub 16 \n\n" +
"я подготовил список всех групп, на которые ты подписан) \n\n" +
"имя группы - 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);
}
}
Здесь каждый тест проверяет отдельный сценарий, а их, напомню, всего пять: - когда просто передали команду /deleteGroupSub и нет подписок на группы;
- когда просто передали команду /deleteGroupSub и есть подписки на группы;
- когда передали невалидный ID группы, например, /deleteGroupSub abc;
- сценарий, при котором все правильно удалится, как и ожидается;
- сценарий, когда ID группы валидный, но такой группы нет в БД.
.put(DELETE_GROUP_SUB.getCommandName(),
new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Теперь можно тестировать функционал уже на тестовом боте. Запускаем нашу базу, используя docker-compose-test.yml:
docker-compose -f docker-compose-test.yml up
И через IDEA запускаем SpringBoot.
Полностью очищу переписку с ботом и начну заново. Прогоню все варианты, которые могут возникнуть при работе с этой командой.Как видно из скриншота, все варианты прошли и прошли успешно.Друзья! Хотите сразу узнавать, когда выйдет новый код по проекту? Когда выходит новая статья? Присоединяйтесь к моему Телеграм-каналу. Там я собираю свою статьи, мысли и open source разработку воедино. |
## 0.6.0-SNAPSHOT
* JRTB-7: added the ability to delete group subscription.
Код работает, тесты на него написаны: пришла пора пушить задачу в репозиторий и создавать пул-реквест.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ