parser
This commit is contained in:
@@ -2,8 +2,10 @@ package kz.konturai.parser;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class ParserApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package kz.konturai.parser.controller;
|
||||
|
||||
import kz.konturai.parser.model.MarketItem;
|
||||
import kz.konturai.parser.service.KursivParserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/parser")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class ParserController {
|
||||
|
||||
@Autowired
|
||||
private KursivParserService kursivParserService;
|
||||
|
||||
/**
|
||||
* Запуск парсинга RSS-ленты
|
||||
*/
|
||||
@PostMapping("/parse")
|
||||
public ResponseEntity<Map<String, Object>> parseRssFeed() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
List<MarketItem> savedItems = kursivParserService.parseAndSaveRssFeed();
|
||||
|
||||
response.put("success", true);
|
||||
response.put("message", "Парсинг завершен успешно");
|
||||
response.put("processedItems", savedItems.size());
|
||||
response.put("totalItemsInDb", kursivParserService.getTotalItemsCount());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "Ошибка при парсинге: " + e.getMessage());
|
||||
response.put("processedItems", 0);
|
||||
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики по сохраненным записям
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<Map<String, Object>> getStats() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
long totalCount = kursivParserService.getTotalItemsCount();
|
||||
|
||||
response.put("success", true);
|
||||
response.put("totalItems", totalCount);
|
||||
response.put("message", "Статистика получена успешно");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "Ошибка при получении статистики: " + e.getMessage());
|
||||
response.put("totalItems", 0);
|
||||
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех сохраненных записей
|
||||
*/
|
||||
@GetMapping("/items")
|
||||
public ResponseEntity<Map<String, Object>> getAllItems() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
List<MarketItem> items = kursivParserService.getAllItems();
|
||||
|
||||
response.put("success", true);
|
||||
response.put("items", items);
|
||||
response.put("count", items.size());
|
||||
response.put("message", "Записи получены успешно");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "Ошибка при получении записей: " + e.getMessage());
|
||||
response.put("items", List.of());
|
||||
response.put("count", 0);
|
||||
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение записи по ID
|
||||
*/
|
||||
@GetMapping("/items/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getItemById(@PathVariable String id) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Здесь можно добавить метод для поиска по ID в сервисе
|
||||
response.put("success", false);
|
||||
response.put("message", "Функция поиска по ID пока не реализована");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "Ошибка при получении записи: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка состояния парсера
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
response.put("status", "UP");
|
||||
response.put("service", "Kursiv RSS Parser");
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
response.put("scheduler", "Enabled - runs every 30 minutes");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о планировщике
|
||||
*/
|
||||
@GetMapping("/scheduler/info")
|
||||
public ResponseEntity<Map<String, Object>> getSchedulerInfo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
response.put("schedulerEnabled", true);
|
||||
response.put("cronExpression", "0 0/30 * * * ?");
|
||||
response.put("description", "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)");
|
||||
response.put("nextRun", "Следующий запуск будет в ближайшие 0 или 30 минут часа");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Document(collection = "market_items")
|
||||
public class MarketItem {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("source_name")
|
||||
private String sourceName;
|
||||
|
||||
@Field("url")
|
||||
private String url;
|
||||
|
||||
@Field("title")
|
||||
private String title;
|
||||
|
||||
@Field("published_at")
|
||||
private LocalDateTime publishedAt;
|
||||
|
||||
@Field("added_at")
|
||||
private LocalDateTime addedAt;
|
||||
|
||||
@Field("raw_text")
|
||||
private String rawText;
|
||||
|
||||
@Field("hash")
|
||||
private String hash;
|
||||
|
||||
@Field("category")
|
||||
private String category;
|
||||
|
||||
@Field("analytics")
|
||||
private Analytics analytics;
|
||||
|
||||
// Конструкторы
|
||||
public MarketItem() {
|
||||
this.addedAt = LocalDateTime.now();
|
||||
this.sourceName = "Kursiv (Бизнес/экономика)";
|
||||
this.category = "Бизнес";
|
||||
this.analytics = new Analytics();
|
||||
}
|
||||
|
||||
public MarketItem(String url, String title, LocalDateTime publishedAt, String rawText, String hash) {
|
||||
this();
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.publishedAt = publishedAt;
|
||||
this.rawText = rawText;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getSourceName() {
|
||||
return sourceName;
|
||||
}
|
||||
|
||||
public void setSourceName(String sourceName) {
|
||||
this.sourceName = sourceName;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public LocalDateTime getPublishedAt() {
|
||||
return publishedAt;
|
||||
}
|
||||
|
||||
public void setPublishedAt(LocalDateTime publishedAt) {
|
||||
this.publishedAt = publishedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getAddedAt() {
|
||||
return addedAt;
|
||||
}
|
||||
|
||||
public void setAddedAt(LocalDateTime addedAt) {
|
||||
this.addedAt = addedAt;
|
||||
}
|
||||
|
||||
public String getRawText() {
|
||||
return rawText;
|
||||
}
|
||||
|
||||
public void setRawText(String rawText) {
|
||||
this.rawText = rawText;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void setHash(String hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public Analytics getAnalytics() {
|
||||
return analytics;
|
||||
}
|
||||
|
||||
public void setAnalytics(Analytics analytics) {
|
||||
this.analytics = analytics;
|
||||
}
|
||||
|
||||
// Вложенный класс для аналитики
|
||||
public static class Analytics {
|
||||
private String summary;
|
||||
private String sentiment;
|
||||
private String[] tags = {};
|
||||
private Map<String, Object> entities = Map.of();
|
||||
|
||||
public Analytics() {
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getSentiment() {
|
||||
return sentiment;
|
||||
}
|
||||
|
||||
public void setSentiment(String sentiment) {
|
||||
this.sentiment = sentiment;
|
||||
}
|
||||
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(String[] tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
public void setEntities(Map<String, Object> entities) {
|
||||
this.entities = entities;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package kz.konturai.parser.repository;
|
||||
|
||||
import kz.konturai.parser.model.MarketItem;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface MarketItemRepository extends MongoRepository<MarketItem, String> {
|
||||
|
||||
/**
|
||||
* Найти запись по хэшу для дедупликации
|
||||
*/
|
||||
Optional<MarketItem> findByHash(String hash);
|
||||
|
||||
/**
|
||||
* Проверить существование записи по хэшу
|
||||
*/
|
||||
boolean existsByHash(String hash);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
import com.rometools.rome.io.XmlReader;
|
||||
import kz.konturai.parser.model.MarketItem;
|
||||
import kz.konturai.parser.repository.MarketItemRepository;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class KursivParserService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KursivParserService.class);
|
||||
|
||||
@Autowired
|
||||
private MarketItemRepository marketItemRepository;
|
||||
|
||||
@Value("${rss.feed.url}")
|
||||
private String rssFeedUrl;
|
||||
|
||||
/**
|
||||
* Автоматический запуск парсинга каждые 30 минут
|
||||
* Cron выражение: "0 0/30 * * * ?" означает запуск в 0 и 30 минут каждого часа
|
||||
*/
|
||||
@Scheduled(cron = "0 0/30 * * * ?")
|
||||
public void scheduledParseAndStoreNews() {
|
||||
logger.info("🔄 Запуск планового парсинга новостей с Kursiv.media...");
|
||||
try {
|
||||
List<MarketItem> savedItems = parseAndSaveRssFeed();
|
||||
logger.info("✅ Плановый парсинг завершен. Обработано записей: {}", savedItems.size());
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ Ошибка при плановом парсинге", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Основной метод для парсинга RSS-ленты и сохранения данных в MongoDB
|
||||
*/
|
||||
public List<MarketItem> parseAndSaveRssFeed() {
|
||||
List<MarketItem> savedItems = new ArrayList<>();
|
||||
|
||||
try {
|
||||
logger.info("Начинаем парсинг RSS-ленты: {}", rssFeedUrl);
|
||||
|
||||
// Получаем RSS-ленту
|
||||
SyndFeed feed = getRssFeed();
|
||||
|
||||
if (feed == null) {
|
||||
logger.error("Не удалось получить RSS-ленту");
|
||||
return savedItems;
|
||||
}
|
||||
|
||||
logger.info("Получено {} записей из RSS-ленты", feed.getEntries().size());
|
||||
|
||||
// Обрабатываем каждую запись
|
||||
for (SyndEntry entry : feed.getEntries()) {
|
||||
try {
|
||||
MarketItem marketItem = processRssEntry(entry);
|
||||
if (marketItem != null) {
|
||||
MarketItem savedItem = saveOrUpdateMarketItem(marketItem);
|
||||
if (savedItem != null) {
|
||||
savedItems.add(savedItem);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обработке записи: {}", entry.getTitle(), e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Успешно обработано {} записей", savedItems.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при парсинге RSS-ленты", e);
|
||||
}
|
||||
|
||||
return savedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение RSS-ленты
|
||||
*/
|
||||
private SyndFeed getRssFeed() throws Exception {
|
||||
URL feedUrl = URI.create(rssFeedUrl).toURL();
|
||||
SyndFeedInput input = new SyndFeedInput();
|
||||
try (XmlReader reader = new XmlReader(feedUrl.openStream())) {
|
||||
return input.build(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка отдельной записи RSS
|
||||
*/
|
||||
private MarketItem processRssEntry(SyndEntry entry) {
|
||||
try {
|
||||
// Извлекаем основные поля
|
||||
String title = entry.getTitle();
|
||||
String url = entry.getLink();
|
||||
String description = getDescription(entry);
|
||||
|
||||
if (title == null || url == null) {
|
||||
logger.warn("Пропускаем запись с отсутствующими обязательными полями: title={}, url={}", title, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Очищаем текст от HTML
|
||||
String cleanText = cleanHtmlText(description);
|
||||
|
||||
// Преобразуем дату
|
||||
LocalDateTime publishedAt = convertToLocalDateTime(entry.getPublishedDate());
|
||||
|
||||
// Генерируем хэш
|
||||
String hash = generateHash(url, title);
|
||||
|
||||
// Создаем объект MarketItem
|
||||
MarketItem marketItem = new MarketItem(url, title, publishedAt, cleanText, hash);
|
||||
|
||||
logger.debug("Обработана запись: {}", title);
|
||||
|
||||
return marketItem;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обработке записи RSS", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлечение описания из записи RSS
|
||||
*/
|
||||
private String getDescription(SyndEntry entry) {
|
||||
if (entry.getDescription() != null) {
|
||||
return entry.getDescription().getValue();
|
||||
}
|
||||
|
||||
// Пробуем получить содержимое из других полей
|
||||
if (entry.getContents() != null && !entry.getContents().isEmpty()) {
|
||||
return entry.getContents().get(0).getValue();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка HTML-тегов из текста
|
||||
*/
|
||||
private String cleanHtmlText(String htmlText) {
|
||||
if (htmlText == null || htmlText.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return Jsoup.parse(htmlText).text();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Ошибка при очистке HTML: {}", e.getMessage());
|
||||
return htmlText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразование даты в LocalDateTime
|
||||
*/
|
||||
private LocalDateTime convertToLocalDateTime(java.util.Date date) {
|
||||
if (date == null) {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
|
||||
return date.toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация SHA-256 хэша для дедупликации
|
||||
*/
|
||||
private String generateHash(String url, String title) {
|
||||
try {
|
||||
String input = url + "::" + title;
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("Ошибка при генерации хэша", e);
|
||||
return String.valueOf((url + "::" + title).hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение или обновление записи в MongoDB (upsert)
|
||||
*/
|
||||
private MarketItem saveOrUpdateMarketItem(MarketItem marketItem) {
|
||||
try {
|
||||
// Проверяем существование записи по хэшу
|
||||
MarketItem existingItem = marketItemRepository.findByHash(marketItem.getHash()).orElse(null);
|
||||
|
||||
if (existingItem != null) {
|
||||
// Обновляем существующую запись
|
||||
existingItem.setTitle(marketItem.getTitle());
|
||||
existingItem.setUrl(marketItem.getUrl());
|
||||
existingItem.setPublishedAt(marketItem.getPublishedAt());
|
||||
existingItem.setRawText(marketItem.getRawText());
|
||||
existingItem.setAddedAt(LocalDateTime.now()); // Обновляем время добавления
|
||||
|
||||
MarketItem savedItem = marketItemRepository.save(existingItem);
|
||||
logger.debug("Обновлена существующая запись: {}", marketItem.getTitle());
|
||||
return savedItem;
|
||||
} else {
|
||||
// Создаем новую запись
|
||||
MarketItem savedItem = marketItemRepository.save(marketItem);
|
||||
logger.debug("Создана новая запись: {}", marketItem.getTitle());
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при сохранении записи: {}", marketItem.getTitle(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики по сохраненным записям
|
||||
*/
|
||||
public long getTotalItemsCount() {
|
||||
return marketItemRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех сохраненных записей
|
||||
*/
|
||||
public List<MarketItem> getAllItems() {
|
||||
return marketItemRepository.findAll();
|
||||
}
|
||||
}
|
||||
@@ -1 +1,17 @@
|
||||
spring.application.name=parser
|
||||
|
||||
# MongoDB Configuration
|
||||
spring.data.mongodb.host=92.38.48.166
|
||||
spring.data.mongodb.port=27017
|
||||
spring.data.mongodb.database=parser_db
|
||||
|
||||
# RSS Feed URL
|
||||
rss.feed.url=https://kursiv.media/feed/
|
||||
|
||||
# Scheduler Configuration
|
||||
spring.task.scheduling.pool.size=2
|
||||
spring.task.scheduling.thread-name-prefix=scheduled-task-
|
||||
|
||||
# Logging Configuration
|
||||
logging.level.kz.konturai.parser.service.KursivParserService=INFO
|
||||
logging.level.org.springframework.scheduling=DEBUG
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package kz.konturai.parser.scheduler;
|
||||
|
||||
import kz.konturai.parser.service.KursivParserService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(properties = {
|
||||
"spring.data.mongodb.host=localhost",
|
||||
"spring.data.mongodb.port=27017",
|
||||
"spring.data.mongodb.database=parser_test_db",
|
||||
"rss.feed.url=https://kursiv.media/feed/"
|
||||
})
|
||||
public class SchedulerTest {
|
||||
|
||||
@Autowired
|
||||
private KursivParserService kursivParserService;
|
||||
|
||||
@Test
|
||||
public void testScheduledMethodExists() {
|
||||
// Проверяем, что метод scheduledParseAndStoreNews существует и доступен
|
||||
assertDoesNotThrow(() -> {
|
||||
// Используем рефлексию для проверки существования метода
|
||||
kursivParserService.getClass().getMethod("scheduledParseAndStoreNews");
|
||||
}, "Метод scheduledParseAndStoreNews должен существовать");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScheduledMethodCanBeCalled() {
|
||||
// Проверяем, что метод можно вызвать без ошибок
|
||||
assertDoesNotThrow(() -> {
|
||||
kursivParserService.scheduledParseAndStoreNews();
|
||||
}, "Метод scheduledParseAndStoreNews должен выполняться без ошибок");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchedulerConfiguration() {
|
||||
// Проверяем, что сервис существует и настроен
|
||||
assertNotNull(kursivParserService, "KursivParserService должен быть настроен");
|
||||
|
||||
// Проверяем, что основной метод парсинга работает
|
||||
assertDoesNotThrow(() -> {
|
||||
kursivParserService.parseAndSaveRssFeed();
|
||||
}, "Основной метод парсинга должен работать");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.model.MarketItem;
|
||||
import kz.konturai.parser.repository.MarketItemRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(properties = {
|
||||
"spring.data.mongodb.host=localhost",
|
||||
"spring.data.mongodb.port=27017",
|
||||
"spring.data.mongodb.database=parser_test_db",
|
||||
"rss.feed.url=https://kursiv.media/feed/"
|
||||
})
|
||||
public class KursivParserServiceTest {
|
||||
|
||||
@Autowired
|
||||
private KursivParserService kursivParserService;
|
||||
|
||||
@Autowired
|
||||
private MarketItemRepository marketItemRepository;
|
||||
|
||||
@Test
|
||||
public void testParseAndSaveRssFeed() {
|
||||
// Очищаем базу данных перед тестом
|
||||
marketItemRepository.deleteAll();
|
||||
|
||||
// Запускаем парсинг
|
||||
List<MarketItem> savedItems = kursivParserService.parseAndSaveRssFeed();
|
||||
|
||||
// Проверяем результаты
|
||||
assertNotNull(savedItems, "Список сохраненных элементов не должен быть null");
|
||||
|
||||
// Проверяем, что хотя бы одна запись была обработана
|
||||
assertTrue(savedItems.size() > 0, "Должна быть обработана хотя бы одна запись");
|
||||
|
||||
// Проверяем структуру первой записи
|
||||
MarketItem firstItem = savedItems.get(0);
|
||||
assertNotNull(firstItem.getTitle(), "Заголовок не должен быть null");
|
||||
assertNotNull(firstItem.getUrl(), "URL не должен быть null");
|
||||
assertNotNull(firstItem.getHash(), "Хэш не должен быть null");
|
||||
assertNotNull(firstItem.getPublishedAt(), "Дата публикации не должна быть null");
|
||||
assertNotNull(firstItem.getRawText(), "Текст не должен быть null");
|
||||
|
||||
// Проверяем, что запись сохранилась в базе данных
|
||||
long totalCount = marketItemRepository.count();
|
||||
assertTrue(totalCount > 0, "В базе данных должны быть записи");
|
||||
|
||||
// Проверяем дедупликацию - повторный запуск не должен создавать дубликаты
|
||||
List<MarketItem> secondRun = kursivParserService.parseAndSaveRssFeed();
|
||||
long secondCount = marketItemRepository.count();
|
||||
|
||||
// Количество записей должно остаться тем же или увеличиться незначительно
|
||||
assertTrue(secondCount >= totalCount, "Количество записей не должно уменьшиться");
|
||||
|
||||
System.out.println("Тест прошел успешно:");
|
||||
System.out.println("- Обработано записей в первом запуске: " + savedItems.size());
|
||||
System.out.println("- Обработано записей во втором запуске: " + secondRun.size());
|
||||
System.out.println("- Общее количество записей в БД: " + secondCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTotalItemsCount() {
|
||||
long count = kursivParserService.getTotalItemsCount();
|
||||
assertTrue(count >= 0, "Количество записей должно быть неотрицательным");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAllItems() {
|
||||
List<MarketItem> items = kursivParserService.getAllItems();
|
||||
assertNotNull(items, "Список элементов не должен быть null");
|
||||
|
||||
// Проверяем структуру элементов
|
||||
for (MarketItem item : items) {
|
||||
assertNotNull(item.getId(), "ID элемента не должен быть null");
|
||||
assertNotNull(item.getHash(), "Хэш элемента не должен быть null");
|
||||
assertNotNull(item.getSourceName(), "Название источника не должно быть null");
|
||||
assertEquals("Kursiv (Бизнес/экономика)", item.getSourceName(),
|
||||
"Название источника должно соответствовать ожидаемому");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user