package org.telegram.abilitybots.api.bot; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.objects.*; import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender; import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Trio; import org.telegram.telegrambots.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.api.objects.*; import org.telegram.telegrambots.exceptions.TelegramApiException; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.Set; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static java.lang.String.format; import static java.util.Collections.emptySet; import static java.util.Optional.empty; import static org.apache.commons.io.FileUtils.deleteQuietly; import static org.apache.commons.lang3.ArrayUtils.addAll; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVERY_MESSAGE; import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVER_SUCCESS; import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.objects.EndUser.endUser; import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT; import static org.telegram.abilitybots.api.objects.Flag.MESSAGE; import static org.telegram.abilitybots.api.objects.Locality.ALL; import static org.telegram.abilitybots.api.objects.Locality.GROUP; import static org.telegram.abilitybots.api.objects.MessageContext.newContext; import static org.telegram.abilitybots.api.objects.Privacy.*; public class AbilityBotTest { private static final String[] EMPTY_ARRAY = {}; private static final long GROUP_ID = 10L; private static final String TEST = "test"; private static final String[] TEXT = {TEST}; public static final EndUser MUSER = endUser(1, "first", "last", "username", null); public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername", null); private DefaultBot bot; private DBContext db; private MessageSender sender; private SilentSender silent; @Before public void setUp() { db = offlineInstance("db"); bot = new DefaultBot(EMPTY, EMPTY, db); sender = mock(MessageSender.class); silent = mock(SilentSender.class); bot.sender = sender; bot.silent = silent; } @Test public void sendsPrivacyViolation() { Update update = mockFullUpdate(MUSER, "/admin"); bot.onUpdateReceived(update); verify(silent, times(1)).send("Sorry, you don't have the required access level to do that.", MUSER.id()); } @Test public void sendsLocalityViolation() { Update update = mockFullUpdate(MUSER, "/group"); bot.onUpdateReceived(update); verify(silent, times(1)).send(format("Sorry, %s-only feature.", "group"), MUSER.id()); } @Test public void sendsInputArgsViolation() { Update update = mockFullUpdate(MUSER, "/count 1 2 3"); bot.onUpdateReceived(update); verify(silent, times(1)).send(format("Sorry, this feature requires %d additional inputs.", 4), MUSER.id()); } @Test public void canProcessRepliesIfSatisfyRequirements() { Update update = mockFullUpdate(MUSER, "must reply"); // False means the update was not pushed down the stream since it has been consumed by the reply assertFalse(bot.filterReply(update)); verify(silent, times(1)).send("reply", MUSER.id()); } @Test public void canBackupDB() throws TelegramApiException { MessageContext context = defaultContext(); bot.backupDB().action().accept(context); deleteQuietly(new java.io.File("backup.json")); verify(sender, times(1)).sendDocument(any()); } @Test public void canRecoverDB() throws TelegramApiException, IOException { Update update = mockBackupUpdate(); Object backup = getDbBackup(); java.io.File backupFile = createBackupFile(backup); when(sender.downloadFile(any(File.class))).thenReturn(backupFile); bot.recoverDB().replies().get(0).actOn(update); verify(silent, times(1)).send(RECOVER_SUCCESS, GROUP_ID); assertEquals("Bot recovered but the DB is still not in sync", db.getSet(TEST), newHashSet(TEST)); assertTrue("Could not delete backup file", backupFile.delete()); } @Test public void canFilterOutReplies() { Update update = mock(Update.class); when(update.hasMessage()).thenReturn(false); assertTrue(bot.filterReply(update)); } @Test public void canDemote() { addUsers(MUSER); bot.admins().add(MUSER.id()); MessageContext context = defaultContext(); bot.demoteAdmin().action().accept(context); Set actual = bot.admins(); Set expected = emptySet(); assertEquals("Could not sudont super-admin", expected, actual); } @Test public void canPromote() { addUsers(MUSER); MessageContext context = defaultContext(); bot.promoteAdmin().action().accept(context); Set actual = bot.admins(); Set expected = newHashSet(MUSER.id()); assertEquals("Could not sudo user", expected, actual); } @Test public void canBanUser() { addUsers(MUSER); MessageContext context = defaultContext(); bot.banUser().action().accept(context); Set actual = bot.blacklist(); Set expected = newHashSet(MUSER.id()); assertEquals("The ban was not emplaced", expected, actual); } @Test public void canUnbanUser() { addUsers(MUSER); bot.blacklist().add(MUSER.id()); MessageContext context = defaultContext(); bot.unbanUser().action().accept(context); Set actual = bot.blacklist(); Set expected = newHashSet(); assertEquals("The ban was not lifted", expected, actual); } @NotNull private MessageContext defaultContext() { MessageContext context = mock(MessageContext.class); when(context.user()).thenReturn(CREATOR); when(context.firstArg()).thenReturn(MUSER.username()); return context; } @Test public void cannotBanCreator() { addUsers(MUSER, CREATOR); MessageContext context = mock(MessageContext.class); when(context.user()).thenReturn(MUSER); when(context.firstArg()).thenReturn(CREATOR.username()); bot.banUser().action().accept(context); Set actual = bot.blacklist(); Set expected = newHashSet(MUSER.id()); assertEquals("Impostor was not added to the blacklist", expected, actual); } private void addUsers(EndUser... users) { Arrays.stream(users).forEach(user -> { bot.users().put(user.id(), user); bot.userIds().put(user.username().toLowerCase(), user.id()); }); } @Test public void creatorCanClaimBot() { MessageContext context = mock(MessageContext.class); when(context.user()).thenReturn(CREATOR); bot.claimCreator().action().accept(context); Set actual = bot.admins(); Set expected = newHashSet(CREATOR.id()); assertEquals("Creator was not properly added to the super admins set", expected, actual); } @Test public void userGetsBannedIfClaimsBot() { addUsers(MUSER); MessageContext context = mock(MessageContext.class); when(context.user()).thenReturn(MUSER); bot.claimCreator().action().accept(context); Set actual = bot.blacklist(); Set expected = newHashSet(MUSER.id()); assertEquals("Could not find user on the blacklist", expected, actual); actual = bot.admins(); expected = emptySet(); assertEquals("Admins set is not empty", expected, actual); } @Test public void bannedCreatorPassesBlacklistCheck() { bot.blacklist().add(CREATOR.id()); Update update = mock(Update.class); Message message = mock(Message.class); User user = mock(User.class); mockUser(update, message, user); boolean notBanned = bot.checkBlacklist(update); assertTrue("Creator is banned", notBanned); } @Test public void canAddUser() { Update update = mock(Update.class); Message message = mock(Message.class); User user = mock(User.class); mockAlternateUser(update, message, user, MUSER); bot.addUser(update); Map expectedUserIds = ImmutableMap.of(MUSER.username(), MUSER.id()); Map expectedUsers = ImmutableMap.of(MUSER.id(), MUSER); assertEquals("User was not added", expectedUserIds, bot.userIds()); assertEquals("User was not added", expectedUsers, bot.users()); } @Test public void canEditUser() { addUsers(MUSER); Update update = mock(Update.class); Message message = mock(Message.class); User user = mock(User.class); String newUsername = MUSER.username() + "-test"; String newFirstName = MUSER.firstName() + "-test"; String newLastName = MUSER.lastName() + "-test"; int sameId = MUSER.id(); EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername, null); mockAlternateUser(update, message, user, changedUser); bot.addUser(update); Map expectedUserIds = ImmutableMap.of(changedUser.username(), changedUser.id()); Map expectedUsers = ImmutableMap.of(changedUser.id(), changedUser); assertEquals("User was not properly edited", bot.userIds(), expectedUserIds); assertEquals("User was not properly edited", expectedUsers, expectedUsers); } @Test public void canValidateAbility() { Trio invalidPair = Trio.of(null, null, null); Ability validAbility = getDefaultBuilder().build(); Trio validPair = Trio.of(null, validAbility, null); assertEquals("Bot can't validate ability properly", false, bot.validateAbility(invalidPair)); assertEquals("Bot can't validate ability properly", true, bot.validateAbility(validPair)); } @Test public void canCheckInput() { Update update = mockFullUpdate(MUSER, "/something"); Ability abilityWithOneInput = getDefaultBuilder() .build(); Ability abilityWithZeroInput = getDefaultBuilder() .input(0) .build(); Trio trioOneArg = Trio.of(update, abilityWithOneInput, TEXT); Trio trioZeroArg = Trio.of(update, abilityWithZeroInput, TEXT); assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioOneArg)); trioOneArg = Trio.of(update, abilityWithOneInput, addAll(TEXT, TEXT)); assertEquals("Unexpected result when applying token filter", false, bot.checkInput(trioOneArg)); assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg)); trioZeroArg = Trio.of(update, abilityWithZeroInput, EMPTY_ARRAY); assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg)); } @Test public void canCheckPrivacy() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); Ability publicAbility = getDefaultBuilder().privacy(PUBLIC).build(); Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); Ability adminAbility = getDefaultBuilder().privacy(ADMIN).build(); Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build(); Trio publicTrio = Trio.of(update, publicAbility, TEXT); Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); Trio adminTrio = Trio.of(update, adminAbility, TEXT); Trio creatorTrio = Trio.of(update, creatorAbility, TEXT); mockUser(update, message, user); assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(publicTrio)); assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(groupAdminTrio)); assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(adminTrio)); assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio)); } @Test public void canValidateGroupAdminPrivacy() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); mockUser(update, message, user); when(message.isGroupMessage()).thenReturn(true); ChatMember member = mock(ChatMember.class); when(member.getUser()).thenReturn(user); when(member.getUser()).thenReturn(user); when(silent.execute(any(GetChatAdministrators.class))).thenReturn(Optional.of(newArrayList(member))); assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(groupAdminTrio)); } @Test public void canRestrictNormalUsersFromGroupAdminAbilities() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); Ability groupAdminAbility = getDefaultBuilder().privacy(GROUP_ADMIN).build(); Trio groupAdminTrio = Trio.of(update, groupAdminAbility, TEXT); mockUser(update, message, user); when(message.isGroupMessage()).thenReturn(true); when(silent.execute(any(GetChatAdministrators.class))).thenReturn(empty()); assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(groupAdminTrio)); } @Test public void canBlockAdminsFromCreatorAbilities() { Update update = mock(Update.class); Message message = mock(Message.class); org.telegram.telegrambots.api.objects.User user = mock(User.class); Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build(); Trio creatorTrio = Trio.of(update, creatorAbility, TEXT); bot.admins().add(MUSER.id()); mockUser(update, message, user); assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio)); } @Test public void canCheckLocality() { Update update = mock(Update.class); Message message = mock(Message.class); User user = mock(User.class); Ability allAbility = getDefaultBuilder().locality(ALL).build(); Ability userAbility = getDefaultBuilder().locality(Locality.USER).build(); Ability groupAbility = getDefaultBuilder().locality(GROUP).build(); Trio publicTrio = Trio.of(update, allAbility, TEXT); Trio userTrio = Trio.of(update, userAbility, TEXT); Trio groupTrio = Trio.of(update, groupAbility, TEXT); mockUser(update, message, user); when(message.isUserMessage()).thenReturn(true); assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(publicTrio)); assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(userTrio)); assertEquals("Unexpected result when checking for locality", false, bot.checkLocality(groupTrio)); } @Test public void canRetrieveContext() { Update update = mock(Update.class); Message message = mock(Message.class); User user = mock(User.class); Ability ability = getDefaultBuilder().build(); Trio trio = Trio.of(update, ability, TEXT); when(message.getChatId()).thenReturn(GROUP_ID); mockUser(update, message, user); Pair actualPair = bot.getContext(trio); Pair expectedPair = Pair.of(newContext(update, MUSER, GROUP_ID, TEXT), ability); assertEquals("Unexpected result when fetching for context", expectedPair, actualPair); } @Test public void defaultGlobalFlagIsTrue() { Update update = mock(Update.class); assertEquals("Unexpected result when checking for the default global flags", true, bot.checkGlobalFlags(update)); } @Test(expected = ArithmeticException.class) public void canConsumeUpdate() { Ability ability = getDefaultBuilder() .action((context) -> { int x = 1 / 0; }).build(); MessageContext context = mock(MessageContext.class); Pair pair = Pair.of(context, ability); bot.consumeUpdate(pair); } @Test public void canFetchAbility() { Update update = mock(Update.class); Message message = mock(Message.class); String text = "/test"; when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); when(update.getMessage().hasText()).thenReturn(true); when(message.getText()).thenReturn(text); Trio trio = bot.getAbility(update); Ability expected = bot.testAbility(); Ability actual = trio.b(); assertEquals("Wrong ability was fetched", expected, actual); } @Test public void canFetchAbilityCaseInsensitive() { Update update = mock(Update.class); Message message = mock(Message.class); String text = "/tESt"; when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); when(update.getMessage().hasText()).thenReturn(true); when(message.getText()).thenReturn(text); Trio trio = bot.getAbility(update); Ability expected = bot.testAbility(); Ability actual = trio.b(); assertEquals("Wrong ability was fetched", expected, actual); } @Test public void canFetchDefaultAbility() { Update update = mock(Update.class); Message message = mock(Message.class); String text = "test tags"; when(update.getMessage()).thenReturn(message); when(message.getText()).thenReturn(text); Trio trio = bot.getAbility(update); Ability expected = bot.defaultAbility(); Ability actual = trio.b(); assertEquals("Wrong ability was fetched", expected, actual); } @Test public void canCheckAbilityFlags() { Update update = mock(Update.class); Message message = mock(Message.class); when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); when(message.hasDocument()).thenReturn(false); when(message.hasText()).thenReturn(true); Ability documentAbility = getDefaultBuilder().flag(DOCUMENT, MESSAGE).build(); Ability textAbility = getDefaultBuilder().flag(Flag.TEXT, MESSAGE).build(); Trio docTrio = Trio.of(update, documentAbility, TEXT); Trio textTrio = Trio.of(update, textAbility, TEXT); assertEquals("Unexpected result when checking for message flags", false, bot.checkMessageFlags(docTrio)); assertEquals("Unexpected result when checking for message flags", true, bot.checkMessageFlags(textTrio)); } @Test public void canReportCommands() { Update update = mock(Update.class); Message message = mock(Message.class); when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); when(message.hasText()).thenReturn(true); MessageContext context = mock(MessageContext.class); when(context.chatId()).thenReturn(GROUP_ID); when(context.user()).thenReturn(MUSER); bot.reportCommands().action().accept(context); verify(silent, times(1)).send("default - dis iz default command", GROUP_ID); } @After public void tearDown() throws IOException { db.clear(); db.close(); } private User mockUser(EndUser fromUser) { User user = mock(User.class); when(user.getId()).thenReturn(fromUser.id()); when(user.getUserName()).thenReturn(fromUser.username()); when(user.getFirstName()).thenReturn(fromUser.firstName()); when(user.getLastName()).thenReturn(fromUser.lastName()); return user; } @NotNull private Update mockFullUpdate(EndUser fromUser, String args) { bot.users().put(MUSER.id(), MUSER); bot.users().put(CREATOR.id(), CREATOR); bot.userIds().put(CREATOR.username(), CREATOR.id()); bot.userIds().put(MUSER.username(), MUSER.id()); bot.admins().add(CREATOR.id()); User user = mockUser(fromUser); Update update = mock(Update.class); when(update.hasMessage()).thenReturn(true); Message message = mock(Message.class); when(message.getFrom()).thenReturn(user); when(message.getText()).thenReturn(args); when(message.hasText()).thenReturn(true); when(message.isUserMessage()).thenReturn(true); when(message.getChatId()).thenReturn((long) fromUser.id()); when(update.getMessage()).thenReturn(message); return update; } private void mockUser(Update update, Message message, User user) { when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); when(message.getFrom()).thenReturn(user); when(user.getFirstName()).thenReturn(MUSER.firstName()); when(user.getLastName()).thenReturn(MUSER.lastName()); when(user.getId()).thenReturn(MUSER.id()); when(user.getUserName()).thenReturn(MUSER.username()); } private void mockAlternateUser(Update update, Message message, User user, EndUser changedUser) { when(user.getId()).thenReturn(changedUser.id()); when(user.getFirstName()).thenReturn(changedUser.firstName()); when(user.getLastName()).thenReturn(changedUser.lastName()); when(user.getUserName()).thenReturn(changedUser.username()); when(message.getFrom()).thenReturn(user); when(update.hasMessage()).thenReturn(true); when(update.getMessage()).thenReturn(message); } private Update mockBackupUpdate() { Update update = mock(Update.class); Message message = mock(Message.class); Message botMessage = mock(Message.class); Document document = mock(Document.class); when(update.getMessage()).thenReturn(message); when(message.getDocument()).thenReturn(document); when(botMessage.getText()).thenReturn(RECOVERY_MESSAGE); when(message.isReply()).thenReturn(true); when(message.hasDocument()).thenReturn(true); when(message.getReplyToMessage()).thenReturn(botMessage); when(message.getChatId()).thenReturn(GROUP_ID); return update; } private Object getDbBackup() { db.getSet(TEST).add(TEST); Object backup = db.backup(); db.clear(); return backup; } private java.io.File createBackupFile(Object backup) throws IOException { java.io.File backupFile = new java.io.File(TEST); BufferedWriter writer = Files.newWriter(backupFile, Charset.defaultCharset()); writer.write(backup.toString()); writer.flush(); writer.close(); return backupFile; } }