diff --git a/.gitignore b/.gitignore index 7ed0d6b..f906b17 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ build/ ### VS Code ### .vscode/ + +### Env variables ### +.env diff --git a/bot/pom.xml b/bot/pom.xml index 58bfbc2..c7a3156 100644 --- a/bot/pom.xml +++ b/bot/pom.xml @@ -66,7 +66,6 @@ lombok true - org.springframework.boot @@ -91,7 +90,7 @@ org.wiremock - wiremock + wiremock-standalone test @@ -124,6 +123,12 @@ kafka test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + diff --git a/bot/src/main/java/edu/java/bot/LinkBot.java b/bot/src/main/java/edu/java/bot/LinkBot.java new file mode 100644 index 0000000..0f8404a --- /dev/null +++ b/bot/src/main/java/edu/java/bot/LinkBot.java @@ -0,0 +1,56 @@ +package edu.java.bot; + +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.UpdatesListener; +import com.pengrad.telegrambot.model.BotCommand; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SetMyCommands; +import edu.java.bot.commands.Command; +import edu.java.bot.configuration.ApplicationConfig; +import edu.java.bot.services.UserMessageProcessor; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + + + +@Component +public class LinkBot extends TelegramBot { + private static final Logger LOGGER = LogManager.getLogger(); + private final UserMessageProcessor userMessageProcessor; + + @Autowired + public LinkBot(ApplicationConfig config, UserMessageProcessor userMessageProcessor, List commands) { + super(config.telegramToken()); + this.userMessageProcessor = userMessageProcessor; + + List botCommands = commands.stream().map(Command::toApiCommand).toList(); + execute(new SetMyCommands(botCommands.toArray(BotCommand[]::new))); + setUpdatesListener(this::processUpdate, e -> { + if (e.response() != null) { + e.response().errorCode(); + e.response().description(); + } else { + LOGGER.error("Some problem with updates from telegram"); + } + }); + } + + public int processUpdate(List updates) { + for (Update update : updates) { + if (!Objects.isNull(update.message())) { + try { + execute(userMessageProcessor.process(update)); + } catch (URISyntaxException e) { + LOGGER.error("Failed to process update", e); + } + } + } + return UpdatesListener.CONFIRMED_UPDATES_ALL; + } + +} diff --git a/bot/src/main/java/edu/java/bot/commands/Command.java b/bot/src/main/java/edu/java/bot/commands/Command.java new file mode 100644 index 0000000..a16c8bb --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/Command.java @@ -0,0 +1,19 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.BotCommand; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; +import java.net.URISyntaxException; + +public interface Command { + String name(); + + String description(); + + SendMessage execute(Update update) throws URISyntaxException; + + default BotCommand toApiCommand() { + return new BotCommand(name(), description()); + } + +} diff --git a/bot/src/main/java/edu/java/bot/commands/CommandDescription.java b/bot/src/main/java/edu/java/bot/commands/CommandDescription.java new file mode 100644 index 0000000..1ae85be --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/CommandDescription.java @@ -0,0 +1,16 @@ +package edu.java.bot.commands; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CommandDescription { + START("/start", "зарегистрировать пользователя"), + HELP("/help", "вывести окно с командами"), + TRACK("/track", "начать отслеживание ссылки"), + UNTRACK("/untrack", "прекратить отслеживание ссылки"), + LIST("/list", "показать список отслеживаемых ссылок"); + private final String name; + private final String description; +} diff --git a/bot/src/main/java/edu/java/bot/commands/Help.java b/bot/src/main/java/edu/java/bot/commands/Help.java new file mode 100644 index 0000000..2dce071 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/Help.java @@ -0,0 +1,37 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.messages.InfoMessage; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class Help implements Command { + private final List allCommands; + private final CommandDescription info = CommandDescription.HELP; + + @Override + public String name() { + return info.getName(); + } + + @Override + public String description() { + return info.getDescription(); + } + + @Override + public SendMessage execute(Update update) { + StringBuilder response = new StringBuilder(); + response.append(InfoMessage.SUPPORTED_COMMANDS.getMessage()); + for (Command botCommand: allCommands) { + response.append(botCommand.name()); + response.append(" : ").append(botCommand.description()).append("\n"); + } + return new SendMessage(update.message().chat().id(), response.toString()); + } + +} diff --git a/bot/src/main/java/edu/java/bot/commands/ListCommand.java b/bot/src/main/java/edu/java/bot/commands/ListCommand.java new file mode 100644 index 0000000..a342a40 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/ListCommand.java @@ -0,0 +1,56 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.messages.ErrorMessage; +import edu.java.bot.messages.InfoMessage; +import edu.java.bot.services.UserManager; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + + + +@Component +@RequiredArgsConstructor +public class ListCommand implements Command { + private final UserManager userManager; + private final CommandDescription info = CommandDescription.LIST; + + @Override + public String name() { + return info.getName(); + } + + @Override + public String description() { + return info.getDescription(); } + + @Override + public SendMessage execute(Update update) { + StringBuilder response = new StringBuilder(); + + Long userId = update.message().from().id(); + Set githubLinks = userManager.getGithubLinks().get(userId); + Set stackOverflowLinks = userManager.getStackOverflowLinks().get(userId); + + if (githubLinks.isEmpty() && stackOverflowLinks.isEmpty()) { + response.append(ErrorMessage.EMPTY_LIST.getMessage()); + } else { + response.append(InfoMessage.LINK_LIST.getMessage()); + if (!githubLinks.isEmpty()) { + response.append(InfoMessage.GITHUB_LINK.getMessage()); + } + for (String link : githubLinks) { + response.append(link).append("\n"); + } + if (!stackOverflowLinks.isEmpty()) { + response.append(InfoMessage.STACKOVERFLOW_LINK.getMessage()); + } + for (String link : stackOverflowLinks) { + response.append(link).append("\n"); + } + } + return new SendMessage(update.message().chat().id(), response.toString()); + } +} diff --git a/bot/src/main/java/edu/java/bot/commands/Start.java b/bot/src/main/java/edu/java/bot/commands/Start.java new file mode 100644 index 0000000..6f0fc4f --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/Start.java @@ -0,0 +1,39 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.messages.ErrorMessage; +import edu.java.bot.messages.SuccessMessage; +import edu.java.bot.services.UserManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class Start implements Command { + private final UserManager userManager; + private final CommandDescription info = CommandDescription.START; + + @Override + public String name() { + return info.getName(); + } + + @Override + public String description() { + return info.getDescription(); } + + @Override + public SendMessage execute(Update update) { + User producer = update.message().from(); + String response = ""; + if (!userManager.containsUser(producer)) { + userManager.add(update.message().from()); + response = SuccessMessage.SIGNUP_SUCCESS.getMessage(); + } else { + response = ErrorMessage.ALREADY_EXIST.getMessage(); + } + return new SendMessage(update.message().chat().id(), response); + } +} diff --git a/bot/src/main/java/edu/java/bot/commands/Track.java b/bot/src/main/java/edu/java/bot/commands/Track.java new file mode 100644 index 0000000..8a75b37 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/Track.java @@ -0,0 +1,34 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.services.UserManager; +import java.net.URISyntaxException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +public class Track implements Command { + private final UserManager userManager; + private final CommandDescription info = CommandDescription.TRACK; + + @Override + public String name() { + return info.getName(); + } + + @Override + public String description() { + return info.getDescription(); + } + + @Override + public SendMessage execute(Update update) throws URISyntaxException { + Long chatId = update.message().chat().id(); + String[] messages = update.message().text().split(" +", 2); + String response = userManager.addLink(update.message().from(), messages[messages.length - 1]); + return new SendMessage(chatId, response); + } +} diff --git a/bot/src/main/java/edu/java/bot/commands/Untrack.java b/bot/src/main/java/edu/java/bot/commands/Untrack.java new file mode 100644 index 0000000..77142b2 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/commands/Untrack.java @@ -0,0 +1,33 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.services.UserManager; +import java.net.URISyntaxException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +public class Untrack implements Command { + private final UserManager userManager; + private final CommandDescription info = CommandDescription.UNTRACK; + + @Override + public String name() { + return info.getName(); + } + + @Override + public String description() { + return info.getDescription(); } + + @Override + public SendMessage execute(Update update) throws URISyntaxException { + Long chatId = update.message().chat().id(); + String[] messages = update.message().text().split(" +", 2); + String response = userManager.removeLink(update.message().from(), messages[messages.length - 1]); + return new SendMessage(chatId, response); + } +} diff --git a/bot/src/main/java/edu/java/bot/configuration/ApplicationConfig.java b/bot/src/main/java/edu/java/bot/configuration/ApplicationConfig.java index 977f29f..b514cff 100644 --- a/bot/src/main/java/edu/java/bot/configuration/ApplicationConfig.java +++ b/bot/src/main/java/edu/java/bot/configuration/ApplicationConfig.java @@ -1,12 +1,13 @@ package edu.java.bot.configuration; import jakarta.validation.constraints.NotEmpty; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @Validated @ConfigurationProperties(prefix = "app", ignoreUnknownFields = false) -public record ApplicationConfig( +public record ApplicationConfig(@Value("${APP_TELEGRAM_TOKEN}") @NotEmpty String telegramToken ) { diff --git a/bot/src/main/java/edu/java/bot/messages/ErrorMessage.java b/bot/src/main/java/edu/java/bot/messages/ErrorMessage.java new file mode 100644 index 0000000..f74eac8 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/messages/ErrorMessage.java @@ -0,0 +1,18 @@ +package edu.java.bot.messages; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorMessage { + ALREADY_EXIST("Пользователь уже зарегистрирован"), + EMPTY_LIST("Список отслеживаемых ссылок пуст. Воспользуйтесь командой /track для добавления ссылки"), + INVALID_URL("Неверный формат ссылки. Попробуйте еще раз"), + UNKNOWN_ERROR("Неизвестная ошибка"), + UNSUPPORTED_LINK("Бот не поддерживает отслеживание данного ресурса"), + UNKNOWN_LINK("Вы не отслеживаете данную ссылку"), + UNKNOWN_COMMAND("Неизвестная команда. Вопользуйтесь командой /help для просмотра доступных команд"), + NO_SUCH_USER("Для использования бота требуется регистрация. Нажмите /start, чтобы зарегистрироваться"); + private final String message; +} diff --git a/bot/src/main/java/edu/java/bot/messages/InfoMessage.java b/bot/src/main/java/edu/java/bot/messages/InfoMessage.java new file mode 100644 index 0000000..c89e63c --- /dev/null +++ b/bot/src/main/java/edu/java/bot/messages/InfoMessage.java @@ -0,0 +1,15 @@ +package edu.java.bot.messages; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum InfoMessage { + SUPPORTED_COMMANDS("Список команд:\n"), + SUPPORTED_SOURCE("Сайты доступные для отслеживания:\n"), + LINK_LIST("Список отслеживаемых ссылок:\n"), + GITHUB_LINK("GitHub:\n"), + STACKOVERFLOW_LINK("StackOverflow:\n"); + private final String message; +} diff --git a/bot/src/main/java/edu/java/bot/messages/SuccessMessage.java b/bot/src/main/java/edu/java/bot/messages/SuccessMessage.java new file mode 100644 index 0000000..a03d1b4 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/messages/SuccessMessage.java @@ -0,0 +1,13 @@ +package edu.java.bot.messages; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SuccessMessage { + SIGNUP_SUCCESS("Вы успешно зарегистрированы в системе. Воспользуйтесь командой /help для вызова справки"), + LINK_ADDED("Ссылка успешно добавлена"), + LINK_REMOVED("Ссылка удалена"); + private final String message; +} diff --git a/bot/src/main/java/edu/java/bot/services/LinkManager.java b/bot/src/main/java/edu/java/bot/services/LinkManager.java new file mode 100644 index 0000000..2af8801 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/services/LinkManager.java @@ -0,0 +1,44 @@ +package edu.java.bot.services; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +@Component +@SuppressWarnings("checkstyle:MemberName") +public class LinkManager implements LinkValidator { + private Logger LOGGER = LogManager.getLogger(); + private final String github = "github.com"; + private final String stackOverflow = "stackoverflow.com"; + private final String linkPattern = + "^(https?)(://)\\w+.\\w{2,}/*[a-zA-Z0-9~@#%&+-=_/|]*"; + + @Override + public boolean isValid(String s) { + return Pattern.matches(linkPattern, s); + } + + @Override + public URI makeURI(String link) throws URISyntaxException { + try { + return new URI(link); + } catch (URISyntaxException e) { + LOGGER.error("ERROR: given link is invalid"); + return null; + } + } + + @Override + public boolean isGitLink(URI link) { + return github.equals(link.getHost()); + } + + @Override + public boolean isStackLink(URI link) { + return stackOverflow.equals(link.getHost()); + } + +} diff --git a/bot/src/main/java/edu/java/bot/services/LinkValidator.java b/bot/src/main/java/edu/java/bot/services/LinkValidator.java new file mode 100644 index 0000000..6e443b0 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/services/LinkValidator.java @@ -0,0 +1,16 @@ +package edu.java.bot.services; + +import java.net.URI; +import java.net.URISyntaxException; + +public interface LinkValidator { + + boolean isValid(String s); + + URI makeURI(String s) throws URISyntaxException; + + boolean isGitLink(URI link); + + boolean isStackLink(URI link); + +} diff --git a/bot/src/main/java/edu/java/bot/services/UserManager.java b/bot/src/main/java/edu/java/bot/services/UserManager.java new file mode 100644 index 0000000..0f7577c --- /dev/null +++ b/bot/src/main/java/edu/java/bot/services/UserManager.java @@ -0,0 +1,64 @@ +package edu.java.bot.services; + +import com.pengrad.telegrambot.model.User; +import edu.java.bot.messages.ErrorMessage; +import edu.java.bot.messages.SuccessMessage; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Getter +@RequiredArgsConstructor +@Repository +public class UserManager { + private final LinkManager linkManager; + private Map allUsers = new HashMap<>(); + private Map> stackOverflowLinks = new HashMap<>(); + private Map> githubLinks = new HashMap<>(); + + public void add(User user) { + allUsers.put(user.id(), user); + stackOverflowLinks.put(user.id(), new HashSet<>()); + githubLinks.put(user.id(), new HashSet<>()); + } + + public boolean containsUser(User user) { + return allUsers.containsKey(user.id()); + } + + + public String addLink(User user, String link) throws URISyntaxException { + if (!linkManager.isValid(link)) { + return ErrorMessage.INVALID_URL.getMessage(); + } + URI uri = linkManager.makeURI(link); + if (linkManager.isGitLink(uri)) { + githubLinks.get(user.id()).add(link); + } else if (linkManager.isStackLink(uri)) { + stackOverflowLinks.get(user.id()).add(link); + } else { + return ErrorMessage.UNSUPPORTED_LINK.getMessage(); + } + return SuccessMessage.LINK_ADDED.getMessage(); + } + + public String removeLink(User user, String link) throws URISyntaxException { + if (!linkManager.isValid(link)) { + return ErrorMessage.INVALID_URL.getMessage(); + } + URI uri = linkManager.makeURI(link); + if (linkManager.isGitLink(uri) && githubLinks.get(user.id()).contains(link)) { + githubLinks.get(user.id()).remove(link); + } else if (linkManager.isStackLink(uri) && stackOverflowLinks.get(user.id()).contains(link)) { + stackOverflowLinks.get(user.id()).remove(link); + } else { + return ErrorMessage.UNKNOWN_LINK.getMessage(); + } + return SuccessMessage.LINK_REMOVED.getMessage(); + } +} diff --git a/bot/src/main/java/edu/java/bot/services/UserMessageProcessor.java b/bot/src/main/java/edu/java/bot/services/UserMessageProcessor.java new file mode 100644 index 0000000..f93e105 --- /dev/null +++ b/bot/src/main/java/edu/java/bot/services/UserMessageProcessor.java @@ -0,0 +1,60 @@ +package edu.java.bot.services; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.request.SendMessage; +import edu.java.bot.commands.Command; +import edu.java.bot.commands.CommandDescription; +import edu.java.bot.messages.ErrorMessage; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserMessageProcessor { + private static final Logger LOGGER = LogManager.getLogger(); + private final UserManager userManager; + private final Map botCommands; + + @Autowired + public UserMessageProcessor(List botCommands, UserManager userManager) { + this.userManager = userManager; + this.botCommands = new HashMap<>(); + + for (Command command: botCommands) { + this.botCommands.put(command.name(), command); + } + } + + public SendMessage process(Update update) throws URISyntaxException { + if (Objects.isNull(update.message())) { + return null; + } + Long chatId = update.message().chat().id(); + User producer = update.message().from(); + String userMessage = update.message().text(); + String commandName = userMessage.split(" +")[0]; + + LOGGER.info("User with chatId = %d has sent %s".formatted(chatId, userMessage)); + if (userManager.containsUser(producer) || commandName.equals(CommandDescription.START.getName())) { + Command command = botCommands.get(commandName); + if (!Objects.isNull(command)) { + LOGGER.info("Executing command: %s".formatted(command.name())); + return command.execute(update); + } else { + LOGGER.info("No such command: %s".formatted(commandName)); + new SendMessage(chatId, ErrorMessage.UNKNOWN_COMMAND.getMessage()); + } + } else { + LOGGER.info("Cannot find user with chatId = %d".formatted(chatId)); + new SendMessage(chatId, ErrorMessage.NO_SUCH_USER.getMessage()); + } + return new SendMessage(chatId, ErrorMessage.UNKNOWN_ERROR.getMessage()); + } +} diff --git a/bot/src/main/resources/application.yml b/bot/src/main/resources/application.yml index 6c549a0..f789ace 100644 --- a/bot/src/main/resources/application.yml +++ b/bot/src/main/resources/application.yml @@ -1,5 +1,5 @@ app: - telegram-token: unset + telegram-token: ${APP_TELEGRAM_TOKEN} spring: application: diff --git a/bot/src/test/java/edu/java/bot/commands/CommandTest.java b/bot/src/test/java/edu/java/bot/commands/CommandTest.java new file mode 100644 index 0000000..e0aafca --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/CommandTest.java @@ -0,0 +1,47 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.model.Chat; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.User; +import edu.java.bot.services.UserManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public abstract class CommandTest { + @Mock + protected UserManager userManager; + + @Mock + protected List allCommands; + + @Mock + protected Update update; + + @Mock + protected Message message; + + @Mock + protected User user; + + @Mock + protected Chat chat; + + + + @BeforeEach + public void setUp() { + lenient().when(update.message()).thenReturn(message); + lenient().when(update.message().from()).thenReturn(user); + lenient().when(user.id()).thenReturn(666L); + lenient().when(message.chat()).thenReturn(chat); + } + + abstract void commandNameTest(); + abstract void commandDescriptionTest(); +} diff --git a/bot/src/test/java/edu/java/bot/commands/HelpTest.java b/bot/src/test/java/edu/java/bot/commands/HelpTest.java new file mode 100644 index 0000000..c62f213 --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/HelpTest.java @@ -0,0 +1,48 @@ +package edu.java.bot.commands; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +public class HelpTest extends CommandTest { + @Autowired + List allCommands; + + @InjectMocks + private Help helpCommand; + + @Test + @Override + void commandNameTest() { + assertEquals(helpCommand.name(), CommandDescription.HELP.getName()); + } + + @Test + @Override + void commandDescriptionTest() { + assertEquals(helpCommand.description(), CommandDescription.HELP.getDescription()); + } + + @Test + void containsSameCommandsTest() { + List expectedNames = List.of("/help", "/list", "/start", "/track", "/untrack"); + List commandNames = allCommands.stream().map(Command::name).toList(); + assertEquals(commandNames, expectedNames); + + } + + @Test + void containsSameDescriptionTest() { + List expectedDescription = List.of("вывести окно с командами", "показать список отслеживаемых ссылок", + "зарегистрировать пользователя", "начать отслеживание ссылки", "прекратить отслеживание ссылки"); + List commandDescription = allCommands.stream().map(Command::description).toList(); + assertEquals(commandDescription, expectedDescription); + } +} diff --git a/bot/src/test/java/edu/java/bot/commands/ListCommandTest.java b/bot/src/test/java/edu/java/bot/commands/ListCommandTest.java new file mode 100644 index 0000000..9838646 --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/ListCommandTest.java @@ -0,0 +1,78 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.request.SendMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ListCommandTest extends CommandTest { + @InjectMocks + private ListCommand listCommand; + + @Test + @Override + void commandNameTest() { + assertEquals(listCommand.name(), CommandDescription.LIST.getName()); + } + + @Test + @Override + void commandDescriptionTest() { + assertEquals(listCommand.description(), CommandDescription.LIST.getDescription()); + } + + @Test + void emptyListTest() { + Long userId = user.id(); + when(userManager.getGithubLinks()).thenReturn(Map.of(userId, new HashSet<>())); + when(userManager.getStackOverflowLinks()).thenReturn(Map.of(userId, new HashSet<>())); + SendMessage result = listCommand.execute(update); + assertEquals("Список отслеживаемых ссылок пуст. Воспользуйтесь командой /track для добавления ссылки", + result.getParameters().get("text")); + } + + @Test + void gitHubListTest() { + Long userId = user.id(); + when(userManager.getGithubLinks()).thenReturn(Map.of(userId, new HashSet<>( + Arrays.asList("https://github.com/rust-lang/rust", + "https://github.com/nodejs/node")))); + when(userManager.getStackOverflowLinks()).thenReturn(Map.of(userId, new HashSet<>())); + String expectedResponse = """ + Список отслеживаемых ссылок: + GitHub: + https://github.com/nodejs/node + https://github.com/rust-lang/rust + """; + SendMessage result = listCommand.execute(update); + assertEquals(expectedResponse, + result.getParameters().get("text")); + } + + @Test + void stackOverflowLinkTest() { + Long userId = user.id(); + when(userManager.getStackOverflowLinks()).thenReturn(Map.of(userId, new HashSet<>( + Arrays.asList("https://stackoverflow.com/questions/78023671/where-is-the-order-in-which-elf-relocations-are-applied-specified", + "https://stackoverflow.com/questions/78049674/regex-to-split-a-column-in-r-after-the-second-pipe-and-after-the-second-t")))); + when(userManager.getGithubLinks()).thenReturn(Map.of(userId, new HashSet<>())); + String expectedResponse = """ + Список отслеживаемых ссылок: + StackOverflow: + https://stackoverflow.com/questions/78049674/regex-to-split-a-column-in-r-after-the-second-pipe-and-after-the-second-t + https://stackoverflow.com/questions/78023671/where-is-the-order-in-which-elf-relocations-are-applied-specified + """; + SendMessage result = listCommand.execute(update); + assertEquals(expectedResponse, + result.getParameters().get("text")); + } +} diff --git a/bot/src/test/java/edu/java/bot/commands/StartTest.java b/bot/src/test/java/edu/java/bot/commands/StartTest.java new file mode 100644 index 0000000..7c55f25 --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/StartTest.java @@ -0,0 +1,48 @@ +package edu.java.bot.commands; + +import com.pengrad.telegrambot.request.SendMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class StartTest extends CommandTest { + + @InjectMocks + private Start startCommand; + + @Test + @Override + void commandNameTest() { + assertEquals(startCommand.name(), CommandDescription.START.getName()); + } + + @Test + @Override + void commandDescriptionTest() { + assertEquals(startCommand.description(), CommandDescription.START.getDescription()); + } + + @Test + void addNewUserTest() { + when(userManager.containsUser(user)).thenReturn(false); + SendMessage result = startCommand.execute(update); + assertEquals("Вы успешно зарегистрированы в системе. Воспользуйтесь командой /help для вызова справки", + result.getParameters().get("text")); + verify(userManager).add(user); + } + + @Test + void addExistingUserTest() { + when(userManager.containsUser(user)).thenReturn(true); + SendMessage result = startCommand.execute(update); + assertEquals("Пользователь уже зарегистрирован", result.getParameters().get("text")); + verify(userManager, never()).add(user); + } + +} diff --git a/bot/src/test/java/edu/java/bot/commands/TrackTest.java b/bot/src/test/java/edu/java/bot/commands/TrackTest.java new file mode 100644 index 0000000..6a6e66b --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/TrackTest.java @@ -0,0 +1,26 @@ +package edu.java.bot.commands; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class TrackTest extends CommandTest { + @InjectMocks + private Track trackCommand; + + @Test + @Override + void commandNameTest() { + assertEquals(trackCommand.name(), CommandDescription.TRACK.getName()); + } + + @Test + @Override + void commandDescriptionTest() { + assertEquals(trackCommand.description(), CommandDescription.TRACK.getDescription()); + } + +} diff --git a/bot/src/test/java/edu/java/bot/commands/UntrackTest.java b/bot/src/test/java/edu/java/bot/commands/UntrackTest.java new file mode 100644 index 0000000..df7da9d --- /dev/null +++ b/bot/src/test/java/edu/java/bot/commands/UntrackTest.java @@ -0,0 +1,26 @@ +package edu.java.bot.commands; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class UntrackTest extends CommandTest { + @InjectMocks + private Untrack untrack; + + @Test + @Override + void commandNameTest() { + assertEquals(untrack.name(), CommandDescription.UNTRACK.getName()); + } + + @Test + @Override + void commandDescriptionTest() { + assertEquals(untrack.description(), CommandDescription.UNTRACK.getDescription()); + } + +} diff --git a/bot/src/test/java/edu/java/bot/services/LinkManagerTest.java b/bot/src/test/java/edu/java/bot/services/LinkManagerTest.java new file mode 100644 index 0000000..51134a0 --- /dev/null +++ b/bot/src/test/java/edu/java/bot/services/LinkManagerTest.java @@ -0,0 +1,56 @@ +package edu.java.bot.services; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import java.net.URI; +import java.net.URISyntaxException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class LinkManagerTest { + private LinkManager linkManager; + @BeforeEach + void init() { + linkManager = new LinkManager(); + } + + @ParameterizedTest + @CsvSource({"https://github.com, true", "affdkmosi.., false"}) + void testIsValid(String link, boolean expected) { + assertEquals(expected, linkManager.isValid(link)); + } + + @ParameterizedTest + @CsvSource({"https://github.com, true", "https://stackoverflow.com, true"}) + void testMakeURI(String link, boolean expected) throws URISyntaxException { + try { + assertEquals(expected, linkManager.makeURI(link) != null); + } catch (URISyntaxException e) { + assertFalse(expected); + } + } + + @ParameterizedTest + @CsvSource({"https://github.com/tamarinvs19/theory_university, true", "https://stackoverflow.com, false", + "https://habr.com/ru/articles/591587/, false"}) + void testIsGitLink(String link, boolean expectedResult) { + try { + assertEquals(expectedResult, linkManager.isGitLink(new URI(link))); + } catch (URISyntaxException e) { + Assertions.fail("invalid URI caused exception"); + } + } + + @ParameterizedTest + @CsvSource({"https://stackoverflow.com, true", "https://github.com, false", + "https://habr.com/ru/articles/591587/, false"}) + void testIsStackLink(String link, boolean expectedResult) { + try { + assertEquals(expectedResult, linkManager.isStackLink(new URI(link))); + } catch (URISyntaxException e) { + Assertions.fail("invalid URI caused exception"); + } + } +} diff --git a/bot/src/test/java/edu/java/bot/services/UserManagerTest.java b/bot/src/test/java/edu/java/bot/services/UserManagerTest.java new file mode 100644 index 0000000..3182f98 --- /dev/null +++ b/bot/src/test/java/edu/java/bot/services/UserManagerTest.java @@ -0,0 +1,80 @@ +package edu.java.bot.services; + +import com.pengrad.telegrambot.model.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UserManagerTest { + private UserManager userManager; + private LinkManager linkManager; + private final Long testId = 123123123L; + private User user; + + @BeforeEach + void setUp() { + linkManager = mock(LinkManager.class); + userManager = new UserManager(linkManager); + user = new User(testId); + } + + @Test + void testAddUser() { + userManager.add(user); + Map allUsers = userManager.getAllUsers(); + assertTrue(allUsers.containsKey(testId)); + assertEquals(user, allUsers.get(testId)); + } + + @Test + void testContainsUser() { + userManager.add(user); + assertTrue(userManager.containsUser(user)); + } + + @Test + void testAddValidLink() throws URISyntaxException { + userManager.add(user); + String validLink = "https://github.com/tamarinvs19/theory_university"; + when(linkManager.isValid(validLink)).thenReturn(true); + URI uri = linkManager.makeURI(validLink); + when(linkManager.isGitLink(uri)).thenReturn(true); + assertEquals("Ссылка успешно добавлена", userManager.addLink(user, validLink)); + assertTrue(userManager.getGithubLinks().get(testId).contains(validLink)); + } + + @Test + void testAddInvalidLink() throws URISyntaxException { + String invalidLink = "invalid"; + when(linkManager.isValid(invalidLink)).thenReturn(false); + assertEquals("Неверный формат ссылки. Попробуйте еще раз", userManager.addLink(user, invalidLink)); + } + + @Test + void testRemoveExistingLink() throws URISyntaxException { + String link = "https://github.com/tamarinvs19/theory_university"; + userManager.add(user); + userManager.getGithubLinks().get(testId).add(link); + when(linkManager.isGitLink(any())).thenReturn(true); + when(linkManager.isValid(any())).thenReturn(true); + assertEquals("Ссылка удалена", userManager.removeLink(user, link)); + assertFalse(userManager.getGithubLinks().get(testId).contains(link)); + } + + @Test + void testRemoveNonExistingLink() throws URISyntaxException { + String link = "https://github.com/tamarinvs19/theory_university"; + userManager.add(user); + when(linkManager.isGitLink(any())).thenReturn(true); + when(linkManager.isValid(any())).thenReturn(true); + assertEquals("Вы не отслеживаете данную ссылку", userManager.removeLink(user, link)); + } +} diff --git a/compose.yml b/compose.yml index 2c41c41..92b6377 100644 --- a/compose.yml +++ b/compose.yml @@ -11,9 +11,26 @@ services: - postgresql:/var/lib/postgresql/data networks: - backend + liquibase-migrations: + image: liquibase/liquibase:4.25 + depends_on: + - postgresql + command: + - --changelog-file=master.xml + - --driver=org.postgresql.Driver + - --url=jdbc:postgresql://postgresql:5432/scrapper + - --username=postgres + - --password=postgres + - update + volumes: + - ./migrations:/liquibase/changelog + networks: + - backend volumes: postgresql: { } networks: backend: { } + + diff --git a/migrations/create_chat.sql b/migrations/create_chat.sql new file mode 100644 index 0000000..5072681 --- /dev/null +++ b/migrations/create_chat.sql @@ -0,0 +1,7 @@ +--liquibase formatted sql +--changeset ado591:01-create_chat.sql +CREATE TABLE IF NOT EXISTS chat +( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL +); diff --git a/migrations/create_chatLink.sql b/migrations/create_chatLink.sql new file mode 100644 index 0000000..c801f2a --- /dev/null +++ b/migrations/create_chatLink.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql +--changeset ado591:03-create-chatLink.sql +CREATE TABLE IF NOT EXISTS chat_link +( + chat_id BIGINT NOT NULL REFERENCES chat (id) ON DELETE CASCADE, + link_id BIGINT NOT NULL REFERENCES link (id) ON DELETE CASCADE + +); diff --git a/migrations/create_link.sql b/migrations/create_link.sql new file mode 100644 index 0000000..d4879e7 --- /dev/null +++ b/migrations/create_link.sql @@ -0,0 +1,10 @@ +--liquibase formatted sql +--changeset ado591:02-create_link.sql +CREATE TABLE IF NOT EXISTS link +( + id BIGINT PRIMARY KEY, + url VARCHAR(255) UNIQUE NOT NULL, + service_name VARCHAR(255) NOT NULL, + last_check_time TIMESTAMP WITH TIME ZONE NOT NULL + +); diff --git a/migrations/master.xml b/migrations/master.xml new file mode 100644 index 0000000..b5bb58b --- /dev/null +++ b/migrations/master.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/pom.xml b/pom.xml index 88280f5..cbbba3a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,11 @@ 3.2.2 2023.0.0 + 3.2.2 + + + 42.7.2 + 4.24.0 24.1.0 @@ -59,7 +64,7 @@ org.wiremock - wiremock + wiremock-standalone ${wiremock.version} @@ -118,6 +123,26 @@ junit-jupiter-params test + + + org.springframework.boot + spring-boot-starter-jdbc + ${spring-jdbc.version} + + + + + org.postgresql + postgresql + ${postgres.version} + runtime + + + org.liquibase + liquibase-core + ${liquibase.version} + test + diff --git a/scrapper/pom.xml b/scrapper/pom.xml index e35ad1f..c0d66c8 100644 --- a/scrapper/pom.xml +++ b/scrapper/pom.xml @@ -85,7 +85,7 @@ org.wiremock - wiremock + wiremock-standalone test @@ -118,6 +118,20 @@ kafka test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.postgresql + postgresql + diff --git a/scrapper/src/main/java/edu/java/clients/GitHubClient.java b/scrapper/src/main/java/edu/java/clients/GitHubClient.java new file mode 100644 index 0000000..9c89ab2 --- /dev/null +++ b/scrapper/src/main/java/edu/java/clients/GitHubClient.java @@ -0,0 +1,24 @@ +package edu.java.clients; + +import edu.java.data.GitDTO; +import org.springframework.web.reactive.function.client.WebClient; + +public class GitHubClient { + private final WebClient webClient; + private String baseUrl = "https://api.github.com"; + + + public GitHubClient(String otherUrl) { + if (!otherUrl.isEmpty()) { + this.baseUrl = otherUrl; + } + this.webClient = WebClient.builder().baseUrl(otherUrl).build(); + } + + public GitDTO fetch(String username, String repositoryName) { + return webClient.get().uri("/repos/%s/%s".formatted(username, repositoryName)) + .retrieve() + .bodyToMono(GitDTO.class) + .block(); + } +} diff --git a/scrapper/src/main/java/edu/java/clients/StackOverflowClient.java b/scrapper/src/main/java/edu/java/clients/StackOverflowClient.java new file mode 100644 index 0000000..b0699f5 --- /dev/null +++ b/scrapper/src/main/java/edu/java/clients/StackOverflowClient.java @@ -0,0 +1,24 @@ +package edu.java.clients; + +import edu.java.data.StackDTO; +import org.springframework.web.reactive.function.client.WebClient; + + +public class StackOverflowClient { + private final WebClient webClient; + private String baseUrl = "https://api.stackexchange.com/2.3"; + + public StackOverflowClient(String otherUrl) { + if (!otherUrl.isEmpty()) { + this.baseUrl = otherUrl; + } + this.webClient = WebClient.builder().baseUrl(otherUrl).build(); + } + + public StackDTO fetch(Long questionId) { + return webClient.get().uri(param -> param + .path("/questions/%d".formatted(questionId)) + .queryParam("site", "stackoverflow").build()) + .retrieve().bodyToMono(StackDTO.class).block(); + } +} diff --git a/scrapper/src/main/java/edu/java/configuration/ClientConfiguration.java b/scrapper/src/main/java/edu/java/configuration/ClientConfiguration.java new file mode 100644 index 0000000..1848350 --- /dev/null +++ b/scrapper/src/main/java/edu/java/configuration/ClientConfiguration.java @@ -0,0 +1,32 @@ +package edu.java.configuration; + +import edu.java.clients.GitHubClient; +import edu.java.clients.StackOverflowClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class ClientConfiguration { + final ApplicationConfig config; + + @Value("${clients.github}") + private String gitBaseUrl; + @Value("${clients.stackoverflow}") + private String stackBaseUrl; + + public ClientConfiguration(ApplicationConfig config) { + this.config = config; + } + + @Bean + public GitHubClient gitHubClient() { + return new GitHubClient(gitBaseUrl); + } + + @Bean + public StackOverflowClient stackOverflowClient() { + return new StackOverflowClient(stackBaseUrl); + } +} diff --git a/scrapper/src/main/java/edu/java/data/GitDTO.java b/scrapper/src/main/java/edu/java/data/GitDTO.java new file mode 100644 index 0000000..7a79663 --- /dev/null +++ b/scrapper/src/main/java/edu/java/data/GitDTO.java @@ -0,0 +1,16 @@ +package edu.java.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +public record GitDTO( + @NotNull + @JsonProperty("name") + String repositoryName, + @NotNull + GitHubUser owner, + @NotNull + @JsonProperty("updated_at") + OffsetDateTime updatedAt +){} diff --git a/scrapper/src/main/java/edu/java/data/GitHubUser.java b/scrapper/src/main/java/edu/java/data/GitHubUser.java new file mode 100644 index 0000000..467a4bd --- /dev/null +++ b/scrapper/src/main/java/edu/java/data/GitHubUser.java @@ -0,0 +1,11 @@ +package edu.java.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + +public record GitHubUser( + @NotNull + @JsonProperty("login") + String username +) { +} diff --git a/scrapper/src/main/java/edu/java/data/StackDTO.java b/scrapper/src/main/java/edu/java/data/StackDTO.java new file mode 100644 index 0000000..38964a1 --- /dev/null +++ b/scrapper/src/main/java/edu/java/data/StackDTO.java @@ -0,0 +1,19 @@ +package edu.java.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +public record StackDTO( + @NotNull + StackUser user, + @NotNull + @JsonProperty("question_id") + Long id, + @NotNull + @JsonProperty("last_activity_date") + OffsetDateTime lastActivity, + @NotNull + @JsonProperty("is_answered") + Boolean isAnswered +){} diff --git a/scrapper/src/main/java/edu/java/data/StackUser.java b/scrapper/src/main/java/edu/java/data/StackUser.java new file mode 100644 index 0000000..b0bb9c9 --- /dev/null +++ b/scrapper/src/main/java/edu/java/data/StackUser.java @@ -0,0 +1,14 @@ +package edu.java.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + +public record StackUser( + @NotNull + @JsonProperty("display_name") + String topicCreator, + @NotNull + @JsonProperty("account_id") + Long accountId +) +{} diff --git a/scrapper/src/main/java/edu/java/scheduler/LinkUpdateScheduler.java b/scrapper/src/main/java/edu/java/scheduler/LinkUpdateScheduler.java new file mode 100644 index 0000000..47896a2 --- /dev/null +++ b/scrapper/src/main/java/edu/java/scheduler/LinkUpdateScheduler.java @@ -0,0 +1,18 @@ +package edu.java.scheduler; + +import lombok.RequiredArgsConstructor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LinkUpdateScheduler { + private static final Logger LOGGER = LogManager.getLogger(); + + @Scheduled(fixedDelayString = "#{@scheduler.interval().toMillis()}") + public void update() { + LOGGER.info("Searching for updates"); + } +} diff --git a/scrapper/src/main/resources/application.yml b/scrapper/src/main/resources/application.yml index d1f6221..0ec6f42 100644 --- a/scrapper/src/main/resources/application.yml +++ b/scrapper/src/main/resources/application.yml @@ -7,9 +7,19 @@ app: spring: application: name: scrapper + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://postgresql:5432/scrapper + username: postgres + password: postgres + server: port: 8080 logging: config: classpath:log4j2-plain.xml + +clients: + stackoverflow: https://api.stackexchange.com/2.3 + github: https://api.github.com diff --git a/scrapper/src/test/java/edu/java/scrapper/CreateDatabasesTest.java b/scrapper/src/test/java/edu/java/scrapper/CreateDatabasesTest.java new file mode 100644 index 0000000..d5f0803 --- /dev/null +++ b/scrapper/src/test/java/edu/java/scrapper/CreateDatabasesTest.java @@ -0,0 +1,45 @@ +package edu.java.scrapper; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.junit.jupiter.Testcontainers; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +public class CreateDatabasesTest extends IntegrationTest { + + @ParameterizedTest + @CsvSource({"SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = 'chat');", + "SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = 'link');", + "SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = 'chat_link');"}) + public void tableExistsTest(String query) { + try ( + Connection connection = DriverManager.getConnection( + postgreSQLContainer.getJdbcUrl(), + postgreSQLContainer.getUsername(), + postgreSQLContainer.getPassword() + ); + Statement statement = connection.createStatement(); + ResultSet changelog = connection + .getMetaData() + .getTables(null, null, "databasechangelog", null); + ResultSet changeloglock = connection + .getMetaData() + .getTables(null, null, "databasechangeloglock", null); + ) { + Assertions.assertTrue(changelog.next()); + Assertions.assertTrue(changeloglock.next()); + ResultSet result = statement.executeQuery(query); + assertTrue(result.next()); + } catch (SQLException sqlException) { + Assertions.fail("sql exception"); + } + } + +} diff --git a/scrapper/src/test/java/edu/java/scrapper/IntegrationTest.java b/scrapper/src/test/java/edu/java/scrapper/IntegrationTest.java index ea5fc28..ed6f270 100644 --- a/scrapper/src/test/java/edu/java/scrapper/IntegrationTest.java +++ b/scrapper/src/test/java/edu/java/scrapper/IntegrationTest.java @@ -1,33 +1,72 @@ package edu.java.scrapper; +import java.io.File; +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import liquibase.Contexts; +import liquibase.LabelExpression; +import liquibase.Liquibase; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.LiquibaseException; +import liquibase.resource.DirectoryResourceAccessor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; @Testcontainers public abstract class IntegrationTest { - public static PostgreSQLContainer POSTGRES; + private static final String DOCKER_IMAGE_NAME = "postgres:16"; + private static final String DATABASE_NAME = "scrapper"; + private static final String DATABASE_USERNAME = "postgres"; + private static final String DATABASE_PASS = "postgres"; + private static final String CHANGELOG_FILE = "master.xml"; + protected static PostgreSQLContainer postgreSQLContainer; static { - POSTGRES = new PostgreSQLContainer<>("postgres:15") - .withDatabaseName("scrapper") - .withUsername("postgres") - .withPassword("postgres"); - POSTGRES.start(); - - runMigrations(POSTGRES); + postgreSQLContainer = new PostgreSQLContainer<>(DockerImageName.parse(DOCKER_IMAGE_NAME)) + .withDatabaseName(DATABASE_NAME) + .withUsername(DATABASE_USERNAME) + .withPassword(DATABASE_PASS); + postgreSQLContainer.start(); + runMigrations(postgreSQLContainer); } - private static void runMigrations(JdbcDatabaseContainer c) { - // ... + private static void runMigrations(JdbcDatabaseContainer jdbcDatabaseContainer) { + Path changelogPath = new File(".").toPath().toAbsolutePath().resolve("../migrations/"); + + try ( + Connection connection = DriverManager.getConnection( + jdbcDatabaseContainer.getJdbcUrl(), + jdbcDatabaseContainer.getUsername(), + jdbcDatabaseContainer.getPassword() + ) + ) { + Database database = DatabaseFactory + .getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(connection)); + + Liquibase liquibase = + new Liquibase(CHANGELOG_FILE, new DirectoryResourceAccessor(changelogPath), database); + liquibase.update(new Contexts(), new LabelExpression()); + } catch (SQLException | LiquibaseException | FileNotFoundException e) { + Assertions.fail("ошибка при выполнении миграции базы данных"); + } } @DynamicPropertySource - static void jdbcProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", POSTGRES::getJdbcUrl); - registry.add("spring.datasource.username", POSTGRES::getUsername); - registry.add("spring.datasource.password", POSTGRES::getPassword); + public static void jdbcProperties(@NotNull DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl); + registry.add("spring.datasource.username", postgreSQLContainer::getUsername); + registry.add("spring.datasource.password", postgreSQLContainer::getPassword); } }