Всім привіт, мої дорогі друзі майбутні Senior Software Engineers. Продовжуємо розробку телеграм-бота. На цьому етапі нашого проекту розглянемо три завдання, у яких більше видимого значення, ніж програмного. Нам потрібно навчитися видаляти передплату на нові статті з певної групи: за допомогою команди / stop деактивувати бота, а за допомогою команди / start - активувати. Причому так, щоб усі запити та оновлення стосувалися лише активних користувачів бота. Як завжди, оновимо main гілка, щоб отримати всі зміни, і створимо нову: STEP_7_JRTB-7. У цій частині поговоримо про видалення передплати та розглянемо 5 варіантів подій – буде цікаво.
!["Java-проект від А до Я": Видаляємо передплату на статті з групи - 1]()
Щоб швидко видалити користувача, я додав анотацію EqualsAndHashCode для TelegramUser, виключивши список GroupSub, щоб не було жодних проблем. І викликав у колекції користувачів метод remove із потрібним нам користувачем. Ось як це виглядає у TelegramUser:
Зрозуміло, щоб ця команда запрацювала, потрібно додати її ініціалізацію до CommandContainer :
Як видно зі скріншоту, всі варіанти пройшли та пройшли успішно.
Оновлюємо версію нашого проекту на 0.6.0-SNAPSHOT Оновлюємо RELEASE_NOTES.md, додаючи опис того, що зроблено у новій версії:
Залишилося всього 5 завдань. Тобто ми з вами вже наприкінці шляху. Залишилося небагато. Особливо його подивитися, що ця серія статей йде із середини вересня, тобто протягом 7 місяців! Я колись придумав цю ідею, не очікував, що буде тааак довго. Водночас результатом я задоволений більш ніж! Друзі, якщо не зрозуміло, що відбувається у статті, ставте запитання у коментарях. Так я знатиму, що щось варто краще описати, а щось додатково пояснити. Ну і як завжди лайк – передплата – дзвіночок, ставте зірку нашому проекту , пишіть коментарі та оцінюйте статтю! Всім дякую. До наступної частини. Скоро поговоримо про те, як додати деактивацію та активацію бота через команди /stop & /start і про те, як їх краще використовувати. До скорого!
JRTB-7: видалення передплати нових статей з групи
Ясна річ, що всім користувачам захочеться мати можливість видалити передплату, щоб не отримувати сповіщення про нові статті. Його логіка буде дуже схожа на логіку додавання передплати. Якщо ми надаємо лише одну команду, у відповідь нам прийде список груп та їх ID, на які користувач уже підписаний, щоб можна було зрозуміти, що саме потрібно видалити. А якщо користувач разом із командою передасть ID групи, ми видалимо передплату. Тож підемо розробляти цю команду з боку телеграм-бота.-
Додамо ім'я нової команди - /deleteGroupSub , а CommandName - рядок:
DELETE_GROUP_SUB("/deleteGroupSub")
-
Далі створимо команду DeleteGroupSubCommand :
package com.github.codegymcommunity.jrtb.command; import com.github.codegymcommunity.jrtb.repository.entity.GroupSub; import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser; import com.github.codegymcommunity.jrtb.service.GroupSubService; import com.github.codegymcommunity.jrtb.service.SendBotMessageService; import com.github.codegymcommunity.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.codegymcommunity.jrtb.command.CommandName.DELETE_GROUP_SUB; import static com.github.codegymcommunity.jrtb.command.CommandUtils.getChatId; import static com.github.codegymcommunity.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)); } }
!["Java-проект від А до Я": Видаляємо передплату на статті з групи - 1](https://cdn.javarush.com/images/article/70607075-1edd-481c-a401-9695c490cf97/800.jpeg)
!["Java-проект від А до Я": Видаляємо передплату на статті з групи - 2](https://cdn.javarush.com/images/article/a632e3f7-86ed-405a-887a-5f44e5fb39f7/800.jpeg)
@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.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import com.github.codegymcommunity.jrtb.service.GroupSubService;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.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.codegymcommunity.jrtb.command.AbstractCommandTest.prepareUpdate;
import static com.github.codegymcommunity.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 групи валідний, але такої групи немає у БД.
!["Java-проект від А до Я": Видаляємо передплату на статті з групи - 3](https://cdn.javarush.com/images/article/dda39455-c7f7-4fab-bf6a-8a1b79a52b52/800.jpeg)
.put(DELETE_GROUP_SUB.getCommandName(),
new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Тепер можна тестувати функціонал на тестовому боті. Запускаємо нашу базу за допомогою docker-compose-test.yml: docker-compose -f docker-compose-test.yml up І через IDEA запускаємо SpringBoot. Повністю очищу листування з ботом і почну заново. Прожену всі варіанти, які можуть виникнути під час роботи з цією командою. !["Java-проект від А до Я": Видаляємо передплату на статті з групи - 4](https://cdn.javarush.com/images/article/6683ae95-de7d-44f2-88e3-52009a06e77e/800.jpeg)
Друзі! Хочете відразу дізнаватися, коли вийде новий код за проектом? Коли виходить нова стаття? Приєднуйтесь до мого Телеграм-каналу . Там я збираю свою статтю, думки і open source розробку воєдино. |
## 0.6.0-SNAPSHOT * JRTB-7: added ability до delete group subscription.
Код працює, тести на нього написані: настав час пушити завдання в репозиторій і створювати пул-реквест.!["Java-проект від А до Я": Видаляємо передплату на статті з групи - 5](https://cdn.javarush.com/images/article/7368adf4-04e6-444c-847f-428cbab3bf4e/1080.jpeg)
Замість закінчення
Давно ми не дивабося на наш борд проекту, адже там відбулися великі зміни:!["Java-проект від А до Я": Видаляємо підписку на статті з групи - 6](https://cdn.javarush.com/images/article/8aef6fd9-bacc-475b-9686-d14914c25424/800.jpeg)