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);
}
}