Compare commits
17 Commits
884b5bf9a0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd62fba75 | ||
|
|
76aa1ab253 | ||
|
|
7438e2b57a | ||
|
|
7ad5361a61 | ||
|
|
e9204516cf | ||
|
|
a4067202ec | ||
|
|
5e77a63aff | ||
|
|
97d6d9adc6 | ||
|
|
b1e79e1e2b | ||
|
|
9813f2489e | ||
|
|
db7bcd1d54 | ||
|
|
f511e6ed77 | ||
|
|
df09da42ef | ||
|
|
f94cb5460d | ||
|
|
c6183a88ef | ||
|
|
01ea006d95 | ||
|
|
1de0988da9 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
target/
|
||||||
|
.git/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Email settings
|
||||||
|
EMAIL_FROM=m.ryabyh@gmail.com
|
||||||
|
EMAIL_TOKEN=your_app_password_here
|
||||||
|
EMAIL_TO=m.ryabyh@gmail.com,AAAtapina@sberbank.ru,Oreshkina.V.V@sberbank.ru,MEvRyabykh@sberbank.ru
|
||||||
|
EMAIL_DAYS=1
|
||||||
|
SPECIAL_EMAIL=m.ryabyh+torgi@gmail.com
|
||||||
|
ENABLE_EMAIL_SCAN=false
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_URL=jdbc:postgresql://localhost:5432/torgi
|
||||||
|
DB_USERNAME=torgi
|
||||||
|
DB_PASSWORD=your_db_password_here
|
||||||
48
.gitea/workflows/cd.yaml
Normal file
48
.gitea/workflows/cd.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.rmevs.vip
|
||||||
|
BACKEND_IMAGE: gitea.rmevs.vip/rm/torgi2-backend
|
||||||
|
FRONTEND_IMAGE: gitea.rmevs.vip/rm/torgi2-frontend
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push backend
|
||||||
|
run: docker build -t "$BACKEND_IMAGE:latest" -f Dockerfile . && docker push "$BACKEND_IMAGE:latest"
|
||||||
|
|
||||||
|
- name: Build and push frontend
|
||||||
|
run: docker build -t "$FRONTEND_IMAGE:latest" ./frontend && docker push "$FRONTEND_IMAGE:latest"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build-and-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to prod over SSH
|
||||||
|
env:
|
||||||
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||||
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||||
|
SSH_PORT: ${{ secrets.SSH_PORT }}
|
||||||
|
SSH_USER: ${{ secrets.SSH_USER }}
|
||||||
|
DEPLOY_DIR: ${{ secrets.SSH_DEPLOY_DIR }}
|
||||||
|
COMPOSE_FILE: ${{ secrets.COMPOSE_FILE }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$SSH_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
PORT="${SSH_PORT:-22}"
|
||||||
|
FILE="${COMPOSE_FILE:-docker-compose.yml}"
|
||||||
|
ssh -i ~/.ssh/deploy_key -p "$PORT" -o StrictHostKeyChecking=no -o BatchMode=yes "$SSH_USER@$SSH_HOST" \
|
||||||
|
"cd $DEPLOY_DIR && git pull --ff-only && docker compose -f \"$FILE\" pull && sudo -n systemctl stop torgi && sudo -n systemctl start torgi && sudo -n systemctl is-active torgi"
|
||||||
16
Dockerfile
16
Dockerfile
@@ -1,3 +1,15 @@
|
|||||||
FROM amazoncorretto:26-alpine-jdk
|
# Этап 1: сборка jar внутри Docker (maven package отдельно запускать не нужно)
|
||||||
COPY torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
|
FROM maven:3.9-eclipse-temurin-17 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY mvnw pom.xml ./
|
||||||
|
COPY .mvn .mvn
|
||||||
|
RUN chmod +x mvnw && ./mvnw -B dependency:go-offline
|
||||||
|
COPY src ./src
|
||||||
|
RUN ./mvnw -B package -DskipTests
|
||||||
|
|
||||||
|
# Этап 2: запуск (JRE 17 — та же версия, что java.version в pom.xml)
|
||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /home
|
||||||
|
COPY --from=build /app/target/torgi-0.0.1-SNAPSHOT.jar /home/torgi-0.0.1-SNAPSHOT.jar
|
||||||
|
EXPOSE 8181
|
||||||
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]
|
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-XX:InitialRAMPercentage=50", "-Xms512m", "-Xmx4096m", "-jar", "/home/torgi-0.0.1-SNAPSHOT.jar"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
client-backend:
|
client-backend:
|
||||||
image: client:0.0.1
|
image: gitea.rmevs.vip/rm/torgi2-backend:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -10,11 +10,13 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- service-db
|
- service-db
|
||||||
environment:
|
environment:
|
||||||
- SERVER_PORT=8181
|
|
||||||
- SERVER_PORT=8181
|
- SERVER_PORT=8181
|
||||||
- SPRING_CLOUD_CONFIG_URI=http://192.168.2.22:10022
|
- SPRING_CLOUD_CONFIG_URI=http://192.168.2.22:10022
|
||||||
- SPRING_CLOUD_CONFIG_PROFILE=prod
|
- SPRING_CLOUD_CONFIG_PROFILE=prod
|
||||||
restart: always
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- monitoring
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: wget --no-verbose --tries=1 --spider http://localhost:8181/health || exit 1
|
test: wget --no-verbose --tries=1 --spider http://localhost:8181/health || exit 1
|
||||||
interval: 20s
|
interval: 20s
|
||||||
@@ -45,7 +47,20 @@ services:
|
|||||||
- "5050:80"
|
- "5050:80"
|
||||||
volumes:
|
volumes:
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
profiles:
|
||||||
|
- tools
|
||||||
|
|
||||||
volumes:
|
frontend:
|
||||||
db-data:
|
image: gitea.rmevs.vip/rm/torgi2-frontend:latest
|
||||||
pgadmin-data:
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- client-backend
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monitoring:
|
||||||
|
external: true
|
||||||
2
frontend/.gitignore
vendored
Normal file
2
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Этап 1: сборка React-приложения.
|
||||||
|
# VITE_API_URL не задаем: фронт ходит на относительный /api,
|
||||||
|
# который этот же nginx проксирует в бэкенд (см. nginx.conf).
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Этап 2: раздача статики + прокси API.
|
||||||
|
# Хост бэкенда задается переменной BACKEND_HOST (шаблон обрабатывается
|
||||||
|
# entrypoint'ом nginx при старте контейнера).
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
ENV BACKEND_HOST=client-backend
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||||
|
EXPOSE 80
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Торги — события</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
frontend/nginx.conf.template
Normal file
30
frontend/nginx.conf.template
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
# API бэкенда. Хост задается переменной BACKEND_HOST:
|
||||||
|
# в нашем docker-compose.yaml это client-backend (дефолт в Dockerfile),
|
||||||
|
# в кастомных файлах переопределите через environment.
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://${BACKEND_HOST}:8181;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Swagger UI бэкенда (удобно на первое время, потом можно закрыть)
|
||||||
|
location /swagger-ui/ {
|
||||||
|
proxy_pass http://${BACKEND_HOST}:8181;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
location /v3/api-docs {
|
||||||
|
proxy_pass http://${BACKEND_HOST}:8181;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Статика React SPA
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
1792
frontend/package-lock.json
generated
Normal file
1792
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "torgi-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/App.tsx
Normal file
10
frontend/src/App.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import EventsPage from './pages/EventsPage';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<main className="container">
|
||||||
|
<h1>Торги — события</h1>
|
||||||
|
<EventsPage />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
frontend/src/api.ts
Normal file
24
frontend/src/api.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Page, TorgiEvent } from './types';
|
||||||
|
|
||||||
|
// В dev-режиме запросы идут через vite-proxy (см. vite.config.ts),
|
||||||
|
// поэтому BASE по умолчанию пустой. Для продакшна задать VITE_API_URL,
|
||||||
|
// например https://torgi.example.com
|
||||||
|
const BASE = import.meta.env.VITE_API_URL ?? '';
|
||||||
|
|
||||||
|
export interface EventFilter {
|
||||||
|
cadastral?: string;
|
||||||
|
status?: string;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEvents(filter: EventFilter): Promise<Page<TorgiEvent>> {
|
||||||
|
const q = new URLSearchParams();
|
||||||
|
if (filter.cadastral) q.set('cadastral', filter.cadastral);
|
||||||
|
if (filter.status) q.set('status', filter.status);
|
||||||
|
q.set('page', String(filter.page));
|
||||||
|
q.set('size', String(filter.size));
|
||||||
|
const res = await fetch(`${BASE}/api/events?${q.toString()}`);
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||||
|
return (await res.json()) as Page<TorgiEvent>;
|
||||||
|
}
|
||||||
54
frontend/src/index.css
Normal file
54
frontend/src/index.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #b00020;
|
||||||
|
}
|
||||||
13
frontend/src/main.tsx
Normal file
13
frontend/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
if (!root) throw new Error('Root element not found');
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
116
frontend/src/pages/EventsPage.tsx
Normal file
116
frontend/src/pages/EventsPage.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { fetchEvents } from '../api';
|
||||||
|
import type { Page, TorgiEvent } from '../types';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
function cell(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') return '—';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventsPage() {
|
||||||
|
const [cadastral, setCadastral] = useState('');
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [applied, setApplied] = useState({ cadastral: '', status: '' });
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [data, setData] = useState<Page<TorgiEvent> | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await fetchEvents({ ...applied, page, size: PAGE_SIZE });
|
||||||
|
setData(result);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [applied, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
setPage(0);
|
||||||
|
setApplied({ cadastral: cadastral.trim(), status: status.trim() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="filters">
|
||||||
|
<input
|
||||||
|
placeholder="Кадастровый номер"
|
||||||
|
value={cadastral}
|
||||||
|
onChange={(e) => setCadastral(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Статус"
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => setStatus(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={onSearch} disabled={loading}>
|
||||||
|
Найти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p>Загрузка…</p>}
|
||||||
|
{error && <p className="error">Ошибка: {error}</p>}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<p className="total">Всего: {data.totalElements}</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Извещение</th>
|
||||||
|
<th>Лот</th>
|
||||||
|
<th>Кадастр</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Нач. цена</th>
|
||||||
|
<th>Итог. цена</th>
|
||||||
|
<th>Дата торгов</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.content.map((ev) => (
|
||||||
|
<tr key={ev.id}>
|
||||||
|
<td>{cell(ev.notice)}</td>
|
||||||
|
<td>{cell(ev.lotNumber)}</td>
|
||||||
|
<td>{cell(ev.cadastral)}</td>
|
||||||
|
<td>{cell(ev.status)}</td>
|
||||||
|
<td>{cell(ev.startPrice)}</td>
|
||||||
|
<td>{cell(ev.finalPrice)}</td>
|
||||||
|
<td>{cell(ev.tradesDate)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{data.content.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7}>Ничего не найдено</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="pagination">
|
||||||
|
<button disabled={page === 0 || loading} onClick={() => setPage((p) => p - 1)}>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
<span>
|
||||||
|
Стр. {data.number + 1} из {Math.max(data.totalPages, 1)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
disabled={data.number + 1 >= data.totalPages || loading}
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
>
|
||||||
|
Вперед →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
frontend/src/types.ts
Normal file
26
frontend/src/types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export interface TorgiEvent {
|
||||||
|
id: number;
|
||||||
|
key: string | null;
|
||||||
|
status: string | null;
|
||||||
|
bidStart: string | null;
|
||||||
|
bidEnd: string | null;
|
||||||
|
tradesDate: string | null;
|
||||||
|
startPrice: string | null;
|
||||||
|
step: string | null;
|
||||||
|
initMoney: string | null;
|
||||||
|
finalPrice: string | null;
|
||||||
|
notice: string | null;
|
||||||
|
lotNumber: number | null;
|
||||||
|
cadastral: string | null;
|
||||||
|
type: string | null;
|
||||||
|
fullInfo: string | null;
|
||||||
|
createdAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<T> {
|
||||||
|
content: T[];
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
number: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
18
frontend/vite.config.ts
Normal file
18
frontend/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// Dev-proxy на бэкенд, чтобы не зависеть от CORS при локальной разработке.
|
||||||
|
// VITE_API_URL имеет приоритет, если задан (например, для продакшн-сборки).
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.VITE_API_URL ?? 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1
lombok.config
Normal file
1
lombok.config
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lombok.addLombokGeneratedAnnotation = true
|
||||||
119
pom.xml
119
pom.xml
@@ -29,6 +29,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<spring-cloud.version>2025.0.0</spring-cloud.version>
|
<spring-cloud.version>2025.0.0</spring-cloud.version>
|
||||||
|
<testcontainers.version>1.21.4</testcontainers.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -57,11 +58,40 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Testcontainers -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-testcontainers</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.retry</groupId>
|
||||||
|
<artifactId>spring-retry</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.8.17</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
@@ -109,6 +139,67 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<configuration>
|
||||||
|
<argLine>"@{jacocoArgLine}" -Xshare:off -Dfile.encoding=UTF-8</argLine>
|
||||||
|
<forkCount>1</forkCount>
|
||||||
|
<reuseForks>true</reuseForks>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.12</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>default-prepare-agent</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<propertyName>jacocoArgLine</propertyName>
|
||||||
|
<!-- Сохраняем файл в корень диска C в папку Temp, где нет кириллицы -->
|
||||||
|
<destFile>C:/Temp/jacoco.exec</destFile>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>default-report</id>
|
||||||
|
<phase>test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<!-- Читаем файл из безопасного места -->
|
||||||
|
<dataFile>C:/Temp/jacoco.exec</dataFile>
|
||||||
|
<outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>default-check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>COMPLEXITY</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.jsonschema2pojo</groupId>
|
<groupId>org.jsonschema2pojo</groupId>
|
||||||
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
|
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
|
||||||
@@ -129,32 +220,4 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
<!-- <build>-->
|
|
||||||
<!-- <plugins>-->
|
|
||||||
<!-- <plugin>-->
|
|
||||||
<!-- <artifactId>maven-assembly-plugin</artifactId>-->
|
|
||||||
<!-- <executions>-->
|
|
||||||
<!-- <execution>-->
|
|
||||||
<!-- <phase>package</phase>-->
|
|
||||||
<!-- <goals>-->
|
|
||||||
<!-- <goal>single</goal>-->
|
|
||||||
<!-- </goals>-->
|
|
||||||
<!-- </execution>-->
|
|
||||||
<!-- </executions>-->
|
|
||||||
<!-- <configuration>-->
|
|
||||||
<!-- <archive>-->
|
|
||||||
<!-- <manifest>-->
|
|
||||||
<!-- <addClasspath>true</addClasspath>-->
|
|
||||||
<!-- <mainClass>com.pledge.torgi.MainApplication</mainClass>-->
|
|
||||||
<!-- </manifest>-->
|
|
||||||
<!-- </archive>-->
|
|
||||||
<!-- <descriptorRefs>-->
|
|
||||||
<!-- <descriptorRef>jar-with-dependencies</descriptorRef>-->
|
|
||||||
<!-- </descriptorRefs>-->
|
|
||||||
<!-- </configuration>-->
|
|
||||||
<!-- </plugin>-->
|
|
||||||
<!-- -->
|
|
||||||
<!-- </plugins>-->
|
|
||||||
<!-- </build>-->
|
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.retry.annotation.EnableRetry;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableJpaRepositories
|
@EnableJpaRepositories
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
@EnableConfigurationProperties
|
@EnableConfigurationProperties
|
||||||
|
@EnableRetry
|
||||||
public class MainApplication {
|
public class MainApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(MainApplication.class, args);
|
SpringApplication.run(MainApplication.class, args);
|
||||||
|
|||||||
18
src/main/java/com/pledge/torgi/config/WebConfig.java
Normal file
18
src/main/java/com/pledge/torgi/config/WebConfig.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.pledge.torgi.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/api/**")
|
||||||
|
.allowedOrigins("http://localhost:5173", "http://localhost:3000")
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowCredentials(false)
|
||||||
|
.maxAge(3600);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.pledge.torgi.controller;
|
package com.pledge.torgi.controller;
|
||||||
|
|
||||||
import com.pledge.torgi.model.Configs;
|
|
||||||
import com.pledge.torgi.model.MissedData;
|
import com.pledge.torgi.model.MissedData;
|
||||||
import com.pledge.torgi.model.TorgiEvent;
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
import com.pledge.torgi.repositories.ConfigsRepository;
|
|
||||||
import com.pledge.torgi.repositories.MissedDataRepository;
|
import com.pledge.torgi.repositories.MissedDataRepository;
|
||||||
import com.pledge.torgi.repositories.TorgiEventRepository;
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
import com.pledge.torgi.service.DownloadHeadlessService;
|
import com.pledge.torgi.service.DownloadHeadlessService;
|
||||||
import com.pledge.torgi.service.Lot;
|
import com.pledge.torgi.service.Lot;
|
||||||
import com.pledge.torgi.service.SendEmailService;
|
import com.pledge.torgi.service.SendEmailService;
|
||||||
import com.pledge.torgi.utils.StartParams;
|
import com.pledge.torgi.utils.StartParams;
|
||||||
import lombok.SneakyThrows;
|
import io.micrometer.core.instrument.Counter;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
@@ -20,14 +19,13 @@ import java.io.File;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import static com.pledge.torgi.model.Configs.LAST_RUN_TIME;
|
|
||||||
import static com.pledge.torgi.utils.AppUtils.CUSTOM_EXCEL_LINK;
|
import static com.pledge.torgi.utils.AppUtils.CUSTOM_EXCEL_LINK;
|
||||||
import static com.pledge.torgi.utils.AppUtils.EXCEL_LINK;
|
import static com.pledge.torgi.utils.AppUtils.EXCEL_LINK;
|
||||||
|
|
||||||
@@ -35,15 +33,26 @@ import static com.pledge.torgi.utils.AppUtils.EXCEL_LINK;
|
|||||||
public class DownloadHeadlessController {
|
public class DownloadHeadlessController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessController.class);
|
private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessController.class);
|
||||||
|
private static final Logger businessLogger = LoggerFactory.getLogger("BUSINESS");
|
||||||
private final Set<String> content = new HashSet<>();
|
private final Set<String> content = new HashSet<>();
|
||||||
private final TorgiEventRepository eventRepository;
|
private final TorgiEventRepository eventRepository;
|
||||||
private final ConfigsRepository configsRepository;
|
|
||||||
private final MissedDataRepository missedDataRepository;
|
private final MissedDataRepository missedDataRepository;
|
||||||
|
private final DownloadHeadlessService downloadHeadlessService;
|
||||||
|
private final Counter diffAdded;
|
||||||
|
private final Counter missedAdded;
|
||||||
|
private final AtomicLong lastPollTimestamp;
|
||||||
|
|
||||||
public DownloadHeadlessController(TorgiEventRepository eventRepository, ConfigsRepository configsRepository, MissedDataRepository missedDataRepository) {
|
public DownloadHeadlessController(TorgiEventRepository eventRepository, MissedDataRepository missedDataRepository,
|
||||||
|
DownloadHeadlessService downloadHeadlessService, MeterRegistry meterRegistry) {
|
||||||
this.eventRepository = eventRepository;
|
this.eventRepository = eventRepository;
|
||||||
this.configsRepository = configsRepository;
|
|
||||||
this.missedDataRepository = missedDataRepository;
|
this.missedDataRepository = missedDataRepository;
|
||||||
|
this.downloadHeadlessService = downloadHeadlessService;
|
||||||
|
this.diffAdded = Counter.builder("torgi.events.added").tag("source", "diff")
|
||||||
|
.description("Torgi events added from scheduled diff").register(meterRegistry);
|
||||||
|
this.missedAdded = Counter.builder("torgi.events.added").tag("source", "missed")
|
||||||
|
.description("Torgi events added from missed-data queue").register(meterRegistry);
|
||||||
|
this.lastPollTimestamp = new AtomicLong(0);
|
||||||
|
meterRegistry.gauge("torgi.last.poll.timestamp", lastPollTimestamp);
|
||||||
eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(0, 10000)).forEach(event -> content.add(event.getKey()));
|
eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(0, 10000)).forEach(event -> content.add(event.getKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,77 +61,30 @@ public class DownloadHeadlessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addNewToDiff() {
|
private void addNewToDiff() {
|
||||||
logger.error("startProcessing");
|
businessLogger.info("startProcessing");
|
||||||
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
|
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(EXCEL_LINK);
|
||||||
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(0, EXCEL_LINK);
|
|
||||||
List<TorgiEvent> newEvents = new ArrayList<>();
|
List<TorgiEvent> newEvents = new ArrayList<>();
|
||||||
|
|
||||||
for (String key : currentContent.keySet()) {
|
for (String key : currentContent.keySet()) {
|
||||||
if (!content.contains(key)) {
|
if (!content.contains(key)) {
|
||||||
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
||||||
content.add(key);
|
content.add(key);
|
||||||
logger.error("added new diff content");
|
businessLogger.info("added new diff content");
|
||||||
|
diffAdded.increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRepository.saveAll(newEvents);
|
eventRepository.saveAll(newEvents);
|
||||||
logger.error("completeProcessing");
|
lastPollTimestamp.set(Instant.now().getEpochSecond());
|
||||||
|
businessLogger.info("completeProcessing readed from torgi = {}, added to DB = {}", currentContent.size(), newEvents.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendNewContent(StartParams params, List<String> cadastral, String email) {
|
public void sendNewContent(StartParams params, List<String> cadastral, String email) {
|
||||||
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
|
|
||||||
LocalDate now = LocalDate.now();
|
LocalDate now = LocalDate.now();
|
||||||
downloadHeadlessService.createNewDocument(eventRepository, cadastral, now);
|
downloadHeadlessService.createNewDocument(eventRepository, cadastral, now);
|
||||||
createArchive(now);
|
createArchive(now);
|
||||||
sendArchive(params, now, email);
|
sendArchive(params, now, email);
|
||||||
removeArchiveAndSource(now);
|
removeArchiveAndSource(now);
|
||||||
logger.error("sended archive");
|
businessLogger.info("sended archive");
|
||||||
}
|
|
||||||
|
|
||||||
public void sendNewContent(StartParams params) {
|
|
||||||
if (alreadyRunnedToday()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
|
|
||||||
LocalDate now = LocalDate.now();
|
|
||||||
downloadHeadlessService.createNewDocument(eventRepository, now);
|
|
||||||
// int i = 0;
|
|
||||||
// while (true) {
|
|
||||||
// List<Lot> partition = eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(i, 30000)).stream().map(Lot::new).toList();
|
|
||||||
// if (partition.isEmpty()) {
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// downloadHeadlessService.createNewDocument(eventRepository, now, i);
|
|
||||||
// i++;
|
|
||||||
// }
|
|
||||||
createArchive(now);
|
|
||||||
sendArchive(params, now);
|
|
||||||
removeArchiveAndSource(now);
|
|
||||||
logger.error("sended archive");
|
|
||||||
saveTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveTime() {
|
|
||||||
List<Configs> configs = configsRepository.findAll().stream().filter(cfg -> cfg.getKey().equals(LAST_RUN_TIME)).toList();
|
|
||||||
if (configs.isEmpty()) {
|
|
||||||
Configs newValue = new Configs();
|
|
||||||
newValue.setId(1L);
|
|
||||||
newValue.setKey(LAST_RUN_TIME);
|
|
||||||
newValue.setValue(LocalDateTime.now().toString());
|
|
||||||
configsRepository.save(newValue);
|
|
||||||
} else {
|
|
||||||
Configs oldValue = configs.get(0);
|
|
||||||
oldValue.setValue(LocalDateTime.now().toString());
|
|
||||||
configsRepository.save(oldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean alreadyRunnedToday() {
|
|
||||||
List<Configs> configs = configsRepository.findAll().stream().filter(cfg -> cfg.getKey().equals(LAST_RUN_TIME)).toList();
|
|
||||||
if (configs.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LocalDateTime fromDB = LocalDateTime.parse(configs.get(0).getValue());
|
|
||||||
return fromDB.format(DateTimeFormatter.ISO_LOCAL_DATE).equals(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeArchiveAndSource(LocalDate now) {
|
private void removeArchiveAndSource(LocalDate now) {
|
||||||
@@ -140,19 +102,6 @@ public class DownloadHeadlessController {
|
|||||||
return directoryToBeDeleted.delete();
|
return directoryToBeDeleted.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendArchive(StartParams params, LocalDate now) {
|
|
||||||
try {
|
|
||||||
new SendEmailService("smtp.gmail.com", 587, params.getFrom(), params.getToken())
|
|
||||||
.sendMail(params.getFrom(),
|
|
||||||
params.getTo(),//"m.ryabyh@gmail.com,MEvRyabykh@sberbank.ru",
|
|
||||||
"Новая Выгрузка по торгам от " + now,
|
|
||||||
"Новая Выгрузка по торгам от " + now,
|
|
||||||
new File(now + ".zip"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendArchive(StartParams params, LocalDate now, String to) {
|
private void sendArchive(StartParams params, LocalDate now, String to) {
|
||||||
try {
|
try {
|
||||||
new SendEmailService("smtp.gmail.com", 587, params.getFrom(), params.getToken())
|
new SendEmailService("smtp.gmail.com", 587, params.getFrom(), params.getToken())
|
||||||
@@ -206,13 +155,12 @@ public class DownloadHeadlessController {
|
|||||||
fis.close();
|
fis.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
public void richData() {
|
public void richData() {
|
||||||
|
try {
|
||||||
List<MissedData> all = missedDataRepository.findAll(PageRequest.of(0, 10)).toList();
|
List<MissedData> all = missedDataRepository.findAll(PageRequest.of(0, 10)).toList();
|
||||||
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
|
|
||||||
for (MissedData missedCadastral : all) {
|
for (MissedData missedCadastral : all) {
|
||||||
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(0, String.format(CUSTOM_EXCEL_LINK, missedCadastral.getCadastral()));
|
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(String.format(CUSTOM_EXCEL_LINK, missedCadastral.getCadastral()));
|
||||||
logger.error("get content by cadastral {} {}", missedCadastral.getCadastral(), currentContent.size());
|
businessLogger.info("get content by cadastral {} {}", missedCadastral.getCadastral(), currentContent.size());
|
||||||
if (!currentContent.isEmpty()) {
|
if (!currentContent.isEmpty()) {
|
||||||
List<TorgiEvent> newEvents = new ArrayList<>();
|
List<TorgiEvent> newEvents = new ArrayList<>();
|
||||||
Set<String> contentByCadastral = new HashSet<>();
|
Set<String> contentByCadastral = new HashSet<>();
|
||||||
@@ -222,7 +170,8 @@ public class DownloadHeadlessController {
|
|||||||
if (!contentByCadastral.contains(key)) {
|
if (!contentByCadastral.contains(key)) {
|
||||||
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
||||||
content.add(key);
|
content.add(key);
|
||||||
logger.error("added new missed content");
|
businessLogger.info("added new missed content");
|
||||||
|
missedAdded.increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRepository.saveAll(newEvents);
|
eventRepository.saveAll(newEvents);
|
||||||
@@ -230,5 +179,8 @@ public class DownloadHeadlessController {
|
|||||||
missedDataRepository.delete(missedCadastral);
|
missedDataRepository.delete(missedCadastral);
|
||||||
Thread.sleep(5000);
|
Thread.sleep(5000);
|
||||||
}
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Exception during rich data", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.pledge.torgi.controller.api;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
|
import org.springframework.data.domain.Example;
|
||||||
|
import org.springframework.data.domain.ExampleMatcher;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/events")
|
||||||
|
public class EventController {
|
||||||
|
|
||||||
|
private final TorgiEventRepository repository;
|
||||||
|
|
||||||
|
public EventController(TorgiEventRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Page<TorgiEvent> list(@RequestParam(required = false) String cadastral,
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
|
||||||
|
TorgiEvent probe = new TorgiEvent();
|
||||||
|
probe.setCadastral(cadastral);
|
||||||
|
probe.setStatus(status);
|
||||||
|
ExampleMatcher matcher = ExampleMatcher.matchingAll()
|
||||||
|
.withIgnoreNullValues()
|
||||||
|
.withIgnoreCase()
|
||||||
|
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING);
|
||||||
|
return repository.findAll(Example.of(probe, matcher), pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public TorgiEvent get(@PathVariable Long id) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Event not found: " + id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.pledge.torgi.controller.api;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.MissedData;
|
||||||
|
import com.pledge.torgi.repositories.MissedDataRepository;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/missed")
|
||||||
|
public class MissedDataController {
|
||||||
|
|
||||||
|
private final MissedDataRepository repository;
|
||||||
|
|
||||||
|
public MissedDataController(MissedDataRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Page<MissedData> list(@PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
|
||||||
|
return repository.findAll(pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.pledge.torgi.service;
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.pledge.torgi.model.TorgiEvent;
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
import com.pledge.torgi.repositories.TorgiEventRepository;
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
@@ -12,11 +10,15 @@ import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.Recover;
|
||||||
|
import org.springframework.retry.annotation.Retryable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
@@ -27,23 +29,48 @@ import java.util.*;
|
|||||||
import static com.pledge.torgi.utils.AppUtils.EMPTY;
|
import static com.pledge.torgi.utils.AppUtils.EMPTY;
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
@Service
|
||||||
public class DownloadHeadlessService {
|
public class DownloadHeadlessService {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessService.class);
|
private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessService.class);
|
||||||
|
private static final Logger businessLogger = LoggerFactory.getLogger("BUSINESS");
|
||||||
|
private static final int COL_TYPE = 1;
|
||||||
|
private static final int COL_NOTICE = 3;
|
||||||
|
private static final int COL_BID_START = 6;
|
||||||
|
private static final int COL_BID_END = 7;
|
||||||
|
private static final int COL_TRADES_DATE = 9;
|
||||||
|
private static final int COL_LOT_NUMBER = 16;
|
||||||
|
private static final int COL_STATUS = 17;
|
||||||
|
private static final int COL_INIT_MONEY = 24;
|
||||||
|
private static final int COL_STEP = 25;
|
||||||
|
private static final int COL_START_PRICE = 26;
|
||||||
|
private static final int COL_FINAL_PRICE = 27;
|
||||||
|
private static final int COL_FULL_INFO = 29;
|
||||||
|
|
||||||
@SneakyThrows
|
@Retryable(retryFor = UncheckedIOException.class, maxAttempts = 11,
|
||||||
public Map<String, Lot> downloadExcel(int retryCount, String link) {
|
backoff = @Backoff(delay = 1000))
|
||||||
|
public Map<String, Lot> downloadExcel(String link) {
|
||||||
|
HttpURLConnection connection;
|
||||||
|
int responseCode;
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(link).openConnection();
|
connection = (HttpURLConnection) new URL(link).openConnection();
|
||||||
connection.setRequestMethod("GET");
|
connection.setRequestMethod("GET");
|
||||||
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
connection.setRequestProperty("Accept", "application/json, text/plain, */*");
|
connection.setRequestProperty("Accept", "application/json, text/plain, */*");
|
||||||
connection.setRequestProperty("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7");
|
connection.setRequestProperty("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7");
|
||||||
connection.setConnectTimeout(5000);
|
connection.setConnectTimeout(5000);
|
||||||
connection.setReadTimeout(10000);
|
connection.setReadTimeout(10000);
|
||||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
responseCode = connection.getResponseCode();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error during processing link {} ", link, e);
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
logger.error("Wrong response code {} during call link {} ", responseCode, link);
|
||||||
|
throw new UncheckedIOException(new IOException("Wrong response code " + responseCode + " during call link " + link));
|
||||||
|
}
|
||||||
|
try (Workbook wb = new XSSFWorkbook(connection.getInputStream())) {
|
||||||
Map<String, Lot> data = new HashMap<>();
|
Map<String, Lot> data = new HashMap<>();
|
||||||
Workbook wb = new XSSFWorkbook(connection.getInputStream());
|
|
||||||
Sheet sheetWithData = wb.getSheetAt(0);
|
Sheet sheetWithData = wb.getSheetAt(0);
|
||||||
int currentRow = 2;
|
int currentRow = 2;
|
||||||
while (sheetWithData.getRow(currentRow) != null) {
|
while (sheetWithData.getRow(currentRow) != null) {
|
||||||
@@ -61,36 +88,36 @@ public class DownloadHeadlessService {
|
|||||||
currentRow++;
|
currentRow++;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} catch (IOException e) {
|
||||||
logger.error("Error in line 99 ");
|
logger.error("Error during processing link {} ", link, e);
|
||||||
Thread.sleep(1000);
|
throw new UncheckedIOException(e);
|
||||||
return downloadExcel(retryCount + 1, link);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error in line 103 " + e);
|
logger.error("Error during parsing content by link {} ", link, e);
|
||||||
if (retryCount > 10) {
|
throw new UncheckedIOException(new IOException("Failed to parse content by link " + link, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Recover
|
||||||
|
public Map<String, Lot> downloadExcelFallback(Exception e, String link) {
|
||||||
|
logger.error("Failed to download excel by link {} after retries", link, e);
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Thread.sleep(1000);
|
|
||||||
return downloadExcel(retryCount + 1, link);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Lot extractDataFromRow(Row row, String cadastral) {
|
private Lot extractDataFromRow(Row row, String cadastral) {
|
||||||
Lot lot = new Lot();
|
Lot lot = new Lot();
|
||||||
lot.setBidStart(row.getCell(6).getStringCellValue());
|
lot.setNotice(row.getCell(COL_NOTICE).getStringCellValue());
|
||||||
lot.setBidEnd(row.getCell(7).getStringCellValue());
|
lot.setBidStart(row.getCell(COL_BID_START).getStringCellValue());
|
||||||
|
lot.setBidEnd(row.getCell(COL_BID_END).getStringCellValue());
|
||||||
|
lot.setTradesDate(row.getCell(COL_TRADES_DATE).getStringCellValue());
|
||||||
lot.setCadastral(cadastral);
|
lot.setCadastral(cadastral);
|
||||||
lot.setLotNumber((int) row.getCell(16).getNumericCellValue());
|
lot.setLotNumber((int) row.getCell(COL_LOT_NUMBER).getNumericCellValue());
|
||||||
lot.setFinalPrice(row.getCell(27).getStringCellValue());
|
lot.setStatus(row.getCell(COL_STATUS).getStringCellValue());
|
||||||
lot.setNotice(row.getCell(3).getStringCellValue());
|
lot.setInitMoney(row.getCell(COL_INIT_MONEY).getStringCellValue());
|
||||||
lot.setInitMoney(row.getCell(24).getStringCellValue());
|
lot.setStep(row.getCell(COL_STEP).getStringCellValue());
|
||||||
lot.setStep(row.getCell(25).getStringCellValue());
|
lot.setStartPrice(row.getCell(COL_START_PRICE).getStringCellValue());
|
||||||
lot.setStartPrice(row.getCell(26).getStringCellValue());
|
lot.setFinalPrice(row.getCell(COL_FINAL_PRICE).getStringCellValue());
|
||||||
lot.setStatus(row.getCell(17).getStringCellValue());
|
lot.setFullInfo(row.getCell(COL_FULL_INFO).getStringCellValue());
|
||||||
lot.setTradesDate(row.getCell(9).getStringCellValue());
|
lot.setType(row.getCell(COL_TYPE).getStringCellValue());
|
||||||
lot.setFullInfo(row.getCell(29).getStringCellValue());
|
|
||||||
lot.setType(row.getCell(1).getStringCellValue());
|
|
||||||
return lot;
|
return lot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,31 +153,6 @@ public class DownloadHeadlessService {
|
|||||||
return cadastrals;
|
return cadastrals;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createNewDocument(TorgiEventRepository eventRepository, LocalDate now) {
|
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
|
||||||
XSSFSheet sheet = workbook.createSheet("sheet");
|
|
||||||
setRowHead(sheet);
|
|
||||||
logger.error("before write body");
|
|
||||||
int i = 0;
|
|
||||||
int currentRow = 1;
|
|
||||||
while (true) {
|
|
||||||
List<Lot> partition = eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(i, 1000)).stream().map(Lot::new).toList();
|
|
||||||
if (partition.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
currentRow = setRowBodyNew(sheet, currentRow, partition);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
//setRowWith(sheet);
|
|
||||||
logger.error("before write to file");
|
|
||||||
try (FileOutputStream fileOut = new FileOutputStream(getFileToSave(now))) {
|
|
||||||
workbook.write(fileOut);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
logger.error("in createNewDocument " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createNewDocument(TorgiEventRepository eventRepository, List<String> cadastrals, LocalDate now) {
|
public void createNewDocument(TorgiEventRepository eventRepository, List<String> cadastrals, LocalDate now) {
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
||||||
XSSFSheet sheet = workbook.createSheet("sheet");
|
XSSFSheet sheet = workbook.createSheet("sheet");
|
||||||
@@ -160,14 +162,12 @@ public class DownloadHeadlessService {
|
|||||||
int currentRow = 1;
|
int currentRow = 1;
|
||||||
setRowBodyNew(sheet, currentRow, data);
|
setRowBodyNew(sheet, currentRow, data);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
businessLogger.info("before write to file");
|
||||||
workbook.write(out);
|
|
||||||
logger.error("before write to file");
|
|
||||||
try (FileOutputStream fileOut = new FileOutputStream(getFileToSave(now))) {
|
try (FileOutputStream fileOut = new FileOutputStream(getFileToSave(now))) {
|
||||||
workbook.write(fileOut);
|
workbook.write(fileOut);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.error("in createNewDocument " + ex.getMessage());
|
logger.error("in createNewDocument", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,10 +179,10 @@ public class DownloadHeadlessService {
|
|||||||
try {
|
try {
|
||||||
events.addAll(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + cadastral + "%"));
|
events.addAll(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + cadastral + "%"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error during process cadastral = " + cadastral + " " + e);
|
logger.error("Error during process cadastral = {}", cadastral, e);
|
||||||
}
|
}
|
||||||
if (i % 100 == 0) {
|
if (i % 100 == 0) {
|
||||||
logger.error("executed = " + i + " added = " + events.size());
|
businessLogger.info("executed = {} added = {}", i ,events.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return events.stream().map(Lot::new).toList();
|
return events.stream().map(Lot::new).toList();
|
||||||
@@ -206,7 +206,7 @@ public class DownloadHeadlessService {
|
|||||||
rowHead.createCell(13).setCellValue("Время получения события");
|
rowHead.createCell(13).setCellValue("Время получения события");
|
||||||
}
|
}
|
||||||
|
|
||||||
private int setRowBodyNew(XSSFSheet sheet, int currentRow, Collection<Lot> contents) throws JsonProcessingException {
|
private int setRowBodyNew(XSSFSheet sheet, int currentRow, Collection<Lot> contents) {
|
||||||
for (Lot lot : contents) {
|
for (Lot lot : contents) {
|
||||||
currentRow = createRow(sheet, currentRow, lot);
|
currentRow = createRow(sheet, currentRow, lot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ public class EmailService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Emails> alreadyProcessed = emailsRepository.findTop10ByOrderByIdDesc();
|
List<Emails> alreadyProcessed = emailsRepository.findTop10ByOrderByIdDesc();
|
||||||
List<EmailData> emailData = new ReceiveEmailService("imap.gmail.com", 993, params.getFrom(), params.getToken(), params.getSpecialEmail(), log)
|
List<EmailData> emailData = createReceiveEmailService(params).downloadEmails(alreadyProcessed);
|
||||||
.downloadEmails(alreadyProcessed);
|
|
||||||
int sendLetters = 0;
|
int sendLetters = 0;
|
||||||
log.info("Extracted letters = {}", emailData.size());
|
log.info("Extracted letters = {}", emailData.size());
|
||||||
for (EmailData data : emailData) {
|
for (EmailData data : emailData) {
|
||||||
@@ -41,6 +40,10 @@ public class EmailService {
|
|||||||
log.info("End email processing, send letters = {}", sendLetters);
|
log.info("End email processing, send letters = {}", sendLetters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected ReceiveEmailService createReceiveEmailService(StartParams params) {
|
||||||
|
return new ReceiveEmailService("imap.gmail.com", 993, params.getFrom(), params.getToken(), params.getSpecialEmail(), log);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean alreadyProcessed(String emailId) {
|
private boolean alreadyProcessed(String emailId) {
|
||||||
return emailsRepository.existsByMailId(emailId);
|
return emailsRepository.existsByMailId(emailId);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/test/java/com/pledge/torgi/DatabaseIntegrationTest.java
Normal file
34
src/test/java/com/pledge/torgi/DatabaseIntegrationTest.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package com.pledge.torgi;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||||
|
import org.springframework.test.context.DynamicPropertySource;
|
||||||
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Testcontainers
|
||||||
|
class DatabaseIntegrationTest {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
|
||||||
|
.withDatabaseName("torgi_test")
|
||||||
|
.withUsername("postgres")
|
||||||
|
.withPassword("postgres");
|
||||||
|
|
||||||
|
@DynamicPropertySource
|
||||||
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
||||||
|
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
||||||
|
registry.add("spring.datasource.username", postgres::getUsername);
|
||||||
|
registry.add("spring.datasource.password", postgres::getPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void connectionEstablished() {
|
||||||
|
assertThat(postgres.isRunning()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
package com.pledge.torgi.controller;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.MissedData;
|
||||||
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
|
import com.pledge.torgi.repositories.MissedDataRepository;
|
||||||
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
|
import com.pledge.torgi.service.DownloadHeadlessService;
|
||||||
|
import com.pledge.torgi.service.Lot;
|
||||||
|
import com.pledge.torgi.service.SendEmailService;
|
||||||
|
import com.pledge.torgi.utils.StartParams;
|
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DownloadHeadlessControllerTest {
|
||||||
|
|
||||||
|
private static final String CADASTRAL = "77:01:0001001:1234";
|
||||||
|
private static final String EVENT_KEY = "Notice1_1_PUBLISHED_" + CADASTRAL;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TorgiEventRepository eventRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MissedDataRepository missedDataRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DownloadHeadlessService downloadHeadlessService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lenient().when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
|
||||||
|
.thenReturn(new PageImpl<>(List.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructor_shouldLoadExistingKeys() {
|
||||||
|
// given
|
||||||
|
TorgiEvent event = new TorgiEvent();
|
||||||
|
event.setKey("test-key");
|
||||||
|
Page<TorgiEvent> page = new PageImpl<>(List.of(event));
|
||||||
|
|
||||||
|
when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
|
||||||
|
.thenReturn(page);
|
||||||
|
|
||||||
|
// when
|
||||||
|
new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(eventRepository).findAllByOrderByCreatedAtDesc(any(PageRequest.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructor_shouldLoadAllKeysIntoContentSet() {
|
||||||
|
// given
|
||||||
|
TorgiEvent event1 = new TorgiEvent();
|
||||||
|
event1.setKey("key-1");
|
||||||
|
TorgiEvent event2 = new TorgiEvent();
|
||||||
|
event2.setKey("key-2");
|
||||||
|
when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
|
||||||
|
.thenReturn(new PageImpl<>(List.of(event1, event2)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(getContent(controller)).containsExactlyInAnyOrder("key-1", "key-2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_shouldSaveNewEvents() throws Exception {
|
||||||
|
// given
|
||||||
|
Lot lot = new Lot();
|
||||||
|
lot.setStatus("PUBLISHED");
|
||||||
|
lot.setNotice("Notice1");
|
||||||
|
lot.setLotNumber(1);
|
||||||
|
lot.setCadastral(CADASTRAL);
|
||||||
|
lot.setFullInfo("Full info");
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.process();
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(eventRepository).saveAll(captor.capture());
|
||||||
|
List<TorgiEvent> saved = captor.getValue();
|
||||||
|
assertThat(saved).hasSize(1);
|
||||||
|
assertThat(saved.get(0).getKey()).isEqualTo(EVENT_KEY);
|
||||||
|
assertThat(saved.get(0).getStatus()).isEqualTo("PUBLISHED");
|
||||||
|
assertThat(saved.get(0).getNotice()).isEqualTo("Notice1");
|
||||||
|
assertThat(saved.get(0).getLotNumber()).isEqualTo(1);
|
||||||
|
assertThat(saved.get(0).getCadastral()).isEqualTo(CADASTRAL);
|
||||||
|
assertThat(saved.get(0).getFullInfo()).isEqualTo("Full info");
|
||||||
|
assertThat(getContent(controller)).contains(EVENT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_shouldSkipAlreadyKnownKeys() throws Exception {
|
||||||
|
// given
|
||||||
|
TorgiEvent existing = new TorgiEvent();
|
||||||
|
existing.setKey(EVENT_KEY);
|
||||||
|
when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
|
||||||
|
.thenReturn(new PageImpl<>(List.of(existing)));
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.process();
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(eventRepository).saveAll(captor.capture());
|
||||||
|
assertThat(captor.getValue()).isEmpty();
|
||||||
|
assertThat(getContent(controller)).containsExactly(EVENT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendNewContent_shouldCreateDocumentSendArchiveAndCleanUp() throws Exception {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setFrom("from@example.com");
|
||||||
|
params.setToken("token");
|
||||||
|
List<String> cadastral = List.of(CADASTRAL);
|
||||||
|
|
||||||
|
try (MockedConstruction<SendEmailService> emailConstruction = mockConstruction(SendEmailService.class)) {
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.sendNewContent(params, cadastral, "to@example.com");
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<LocalDate> dateCaptor = ArgumentCaptor.forClass(LocalDate.class);
|
||||||
|
verify(downloadHeadlessService).createNewDocument(eq(eventRepository), eq(cadastral), dateCaptor.capture());
|
||||||
|
LocalDate now = dateCaptor.getValue();
|
||||||
|
|
||||||
|
SendEmailService emailService = emailConstruction.constructed().get(0);
|
||||||
|
verify(emailService).sendMail(eq("from@example.com"), eq("to@example.com"),
|
||||||
|
eq("Новая Выгрузка по торгам от " + now),
|
||||||
|
eq("Новая Выгрузка по торгам от " + now),
|
||||||
|
argThat((File file) -> file != null && file.getName().equals(now + ".zip")));
|
||||||
|
|
||||||
|
assertThat(new File(now + ".zip")).doesNotExist();
|
||||||
|
assertThat(new File(now.toString())).doesNotExist();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendArchive_shouldSwallowEmailException() throws Exception {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setFrom("from@example.com");
|
||||||
|
params.setToken("token");
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
try (MockedConstruction<SendEmailService> ignored = mockConstruction(SendEmailService.class,
|
||||||
|
(mock, context) -> doThrow(new RuntimeException("smtp failure"))
|
||||||
|
.when(mock).sendMail(anyString(), anyString(), anyString(), anyString(), any(File.class)))) {
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
assertThatCode(() -> invokeSendArchive(controller, params, LocalDate.now(), "to@example.com"))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
SendEmailService emailService = ignored.constructed().get(0);
|
||||||
|
verify(emailService).sendMail(anyString(), anyString(), anyString(), anyString(), any(File.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void richData_shouldSaveNewEventsForMissedCadastral() throws Exception {
|
||||||
|
// given
|
||||||
|
MissedData missed = new MissedData();
|
||||||
|
missed.setCadastral(CADASTRAL);
|
||||||
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + CADASTRAL + "%")).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
Lot lot = new Lot();
|
||||||
|
lot.setStatus("PUBLISHED");
|
||||||
|
lot.setNotice("Notice1");
|
||||||
|
lot.setLotNumber(1);
|
||||||
|
lot.setCadastral(CADASTRAL);
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.richData();
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(eventRepository).saveAll(captor.capture());
|
||||||
|
assertThat(captor.getValue()).hasSize(1);
|
||||||
|
assertThat(captor.getValue().get(0).getKey()).isEqualTo(EVENT_KEY);
|
||||||
|
verify(missedDataRepository).delete(missed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void richData_shouldSkipKnownEventsForCadastral() throws Exception {
|
||||||
|
// given
|
||||||
|
MissedData missed = new MissedData();
|
||||||
|
missed.setCadastral(CADASTRAL);
|
||||||
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
||||||
|
|
||||||
|
TorgiEvent existing = new TorgiEvent();
|
||||||
|
existing.setKey(EVENT_KEY);
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + CADASTRAL + "%")).thenReturn(List.of(existing));
|
||||||
|
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.richData();
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(eventRepository).saveAll(captor.capture());
|
||||||
|
assertThat(captor.getValue()).isEmpty();
|
||||||
|
verify(missedDataRepository).delete(missed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void richData_shouldDeleteMissedDataWhenDownloadedContentEmpty() throws Exception {
|
||||||
|
// given
|
||||||
|
MissedData missed = new MissedData();
|
||||||
|
missed.setCadastral(CADASTRAL);
|
||||||
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(Collections.emptyMap());
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.richData();
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(eventRepository, never()).saveAll(anyList());
|
||||||
|
verify(missedDataRepository).delete(missed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void richData_shouldAbortProcessingOnRepositoryError() throws Exception {
|
||||||
|
// given
|
||||||
|
MissedData missed = new MissedData();
|
||||||
|
missed.setCadastral(CADASTRAL);
|
||||||
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc(anyString()))
|
||||||
|
.thenThrow(new RuntimeException("Database error"));
|
||||||
|
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.richData();
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(eventRepository, never()).saveAll(anyList());
|
||||||
|
verify(missedDataRepository, never()).delete(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void richData_shouldHandleExceptionGracefully() {
|
||||||
|
// given
|
||||||
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenThrow(new RuntimeException("Database error"));
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
assertThatCode(controller::richData).doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteDirectory_shouldDeleteNestedStructure(@TempDir File tempDir) throws Exception {
|
||||||
|
// given
|
||||||
|
File nested = new File(tempDir, "nested");
|
||||||
|
File deeper = new File(nested, "deeper");
|
||||||
|
assertThat(deeper.mkdirs()).isTrue();
|
||||||
|
writeFile(new File(deeper, "file.txt"), "content");
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
// when
|
||||||
|
boolean result = invokeDeleteDirectory(controller, nested);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
assertThat(nested).doesNotExist();
|
||||||
|
assertThat(tempDir).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zipFile_shouldAddFileEntry(@TempDir File tempDir) throws Exception {
|
||||||
|
// given
|
||||||
|
File file = new File(tempDir, "data.txt");
|
||||||
|
writeFile(file, "hello");
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// when
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
|
||||||
|
invokeZipFile(file, "dir/data.txt", zipOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
|
||||||
|
ZipEntry entry = zipIn.getNextEntry();
|
||||||
|
assertThat(entry).isNotNull();
|
||||||
|
assertThat(entry.getName()).isEqualTo("dir/data.txt");
|
||||||
|
assertThat(new String(zipIn.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("hello");
|
||||||
|
assertThat(zipIn.getNextEntry()).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zipFile_shouldAddDirectoryEntry(@TempDir File tempDir) throws Exception {
|
||||||
|
// given
|
||||||
|
File dir = new File(tempDir, "mydir");
|
||||||
|
assertThat(dir.mkdirs()).isTrue();
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// when
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
|
||||||
|
invokeZipFile(dir, "mydir", zipOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
|
||||||
|
ZipEntry entry = zipIn.getNextEntry();
|
||||||
|
assertThat(entry).isNotNull();
|
||||||
|
assertThat(entry.getName()).isEqualTo("mydir/");
|
||||||
|
assertThat(entry.isDirectory()).isTrue();
|
||||||
|
assertThat(zipIn.getNextEntry()).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createArchive_shouldCreateZipFileForDirectory() throws Exception {
|
||||||
|
// given
|
||||||
|
LocalDate now = LocalDate.of(1999, 1, 1);
|
||||||
|
File sourceDir = new File(now.toString());
|
||||||
|
File nested = new File(sourceDir, "nested");
|
||||||
|
assertThat(nested.mkdirs()).isTrue();
|
||||||
|
writeFile(new File(nested, "data.txt"), "archive-content");
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// when
|
||||||
|
invokeCreateArchive(controller, now);
|
||||||
|
|
||||||
|
// then
|
||||||
|
File zip = new File(now + ".zip");
|
||||||
|
assertThat(zip).exists();
|
||||||
|
assertThat(readEntryNames(zip)).containsExactlyInAnyOrder(
|
||||||
|
"1999-01-01/", "1999-01-01/nested/", "1999-01-01/nested/data.txt");
|
||||||
|
} finally {
|
||||||
|
new File(now + ".zip").delete();
|
||||||
|
deleteRecursively(sourceDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_shouldIncrementMetrics() throws Exception {
|
||||||
|
// given
|
||||||
|
Lot lot = new Lot();
|
||||||
|
lot.setStatus("PUBLISHED");
|
||||||
|
lot.setNotice("Notice1");
|
||||||
|
lot.setLotNumber(1);
|
||||||
|
lot.setCadastral(CADASTRAL);
|
||||||
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
||||||
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
|
SimpleMeterRegistry registry = new SimpleMeterRegistry();
|
||||||
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, registry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
controller.process();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(registry.get("torgi.events.added").tag("source", "diff").counter().count()).isEqualTo(1.0);
|
||||||
|
assertThat(registry.get("torgi.last.poll.timestamp").gauge().value()).isGreaterThan(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Set<String> getContent(DownloadHeadlessController controller) {
|
||||||
|
try {
|
||||||
|
Field field = DownloadHeadlessController.class.getDeclaredField("content");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Set<String>) field.get(controller);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeCreateArchive(DownloadHeadlessController controller, LocalDate now) {
|
||||||
|
try {
|
||||||
|
Method method = DownloadHeadlessController.class.getDeclaredMethod("createArchive", LocalDate.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(controller, now);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean invokeDeleteDirectory(DownloadHeadlessController controller, File directory) {
|
||||||
|
try {
|
||||||
|
Method method = DownloadHeadlessController.class.getDeclaredMethod("deleteDirectory", File.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (boolean) method.invoke(controller, directory);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeSendArchive(DownloadHeadlessController controller, StartParams params, LocalDate now, String to) {
|
||||||
|
try {
|
||||||
|
Method method = DownloadHeadlessController.class.getDeclaredMethod("sendArchive", StartParams.class, LocalDate.class, String.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(controller, params, now, to);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void invokeZipFile(File fileToZip, String fileName, ZipOutputStream zipOut) {
|
||||||
|
try {
|
||||||
|
Method method = DownloadHeadlessController.class.getDeclaredMethod("zipFile", File.class, String.class, ZipOutputStream.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(null, fileToZip, fileName, zipOut);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFile(File file, String content) throws IOException {
|
||||||
|
Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> readEntryNames(File zipFile) throws IOException {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFile))) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zipIn.getNextEntry()) != null) {
|
||||||
|
names.add(entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteRecursively(File file) {
|
||||||
|
File[] children = file.listFiles();
|
||||||
|
if (children != null) {
|
||||||
|
for (File child : children) {
|
||||||
|
deleteRecursively(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.pledge.torgi.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class HealthControllerTest {
|
||||||
|
|
||||||
|
private final HealthController controller = new HealthController();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getHealth_shouldReturnOk() {
|
||||||
|
// when
|
||||||
|
ResponseEntity<String> response = controller.getPerson();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
|
||||||
|
assertThat(response.getBody()).isEqualTo("OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.pledge.torgi.controller.api;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.domain.Example;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class EventControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TorgiEventRepository repository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private EventController controller;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void list_shouldPassFiltersAndPageableToRepository() {
|
||||||
|
// given
|
||||||
|
TorgiEvent event = new TorgiEvent();
|
||||||
|
event.setKey("key-1");
|
||||||
|
Page<TorgiEvent> page = new PageImpl<>(List.of(event));
|
||||||
|
when(repository.findAll(any(Example.class), any(Pageable.class))).thenReturn(page);
|
||||||
|
Pageable pageable = PageRequest.of(0, 20);
|
||||||
|
|
||||||
|
// when
|
||||||
|
Page<TorgiEvent> result = controller.list("77:01", "PUBLISHED", pageable);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result.getContent()).containsExactly(event);
|
||||||
|
ArgumentCaptor<Example<TorgiEvent>> exampleCaptor = ArgumentCaptor.forClass(Example.class);
|
||||||
|
verify(repository).findAll(exampleCaptor.capture(), eq(pageable));
|
||||||
|
assertThat(exampleCaptor.getValue().getProbe().getCadastral()).isEqualTo("77:01");
|
||||||
|
assertThat(exampleCaptor.getValue().getProbe().getStatus()).isEqualTo("PUBLISHED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void get_shouldReturnEvent() {
|
||||||
|
// given
|
||||||
|
TorgiEvent event = new TorgiEvent();
|
||||||
|
event.setKey("key-1");
|
||||||
|
when(repository.findById(1L)).thenReturn(Optional.of(event));
|
||||||
|
|
||||||
|
// when
|
||||||
|
TorgiEvent result = controller.get(1L);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isSameAs(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void get_shouldThrow404WhenMissing() {
|
||||||
|
// given
|
||||||
|
when(repository.findById(42L)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
assertThatThrownBy(() -> controller.get(42L))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("404");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package com.pledge.torgi.controller.storage;
|
||||||
|
|
||||||
|
import com.pledge.torgi.controller.DownloadHeadlessController;
|
||||||
|
import com.pledge.torgi.model.MissedData;
|
||||||
|
import com.pledge.torgi.repositories.MissedDataRepository;
|
||||||
|
import com.pledge.torgi.utils.StartParams;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class FileServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DownloadHeadlessController headlessController;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private StartParams params;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MissedDataRepository missedDataRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileServiceImpl fileService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractSingleCadastral() throws IOException {
|
||||||
|
// given
|
||||||
|
Workbook workbook = createWorkbookWithData("77:01:0001001:1234");
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = invokeExtractCadastral(workbook);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractMultipleCadastrals() throws IOException {
|
||||||
|
// given
|
||||||
|
Workbook workbook = createWorkbookWithData("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = invokeExtractCadastral(workbook);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldReturnEmptyForEmptyCell() throws IOException {
|
||||||
|
// given
|
||||||
|
Workbook workbook = createWorkbookWithData("");
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = invokeExtractCadastral(workbook);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldReturnEmptyForWorkbookWithNoRows() throws IOException {
|
||||||
|
// given
|
||||||
|
Workbook workbook = new XSSFWorkbook();
|
||||||
|
workbook.createSheet("Sheet1");
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = invokeExtractCadastral(workbook);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processSync_shouldProcessFile() throws IOException {
|
||||||
|
// given
|
||||||
|
Workbook workbook = createWorkbookWithData("77:01:0001001:1234");
|
||||||
|
when(missedDataRepository.findAll()).thenReturn(List.of());
|
||||||
|
|
||||||
|
// when
|
||||||
|
String result = fileService.processSync(workbook, "test@example.com");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEqualTo("Обработано 1 кадастровых номеров");
|
||||||
|
verify(headlessController).sendNewContent(any(), anyList(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processAsync_shouldProcessFile() throws IOException {
|
||||||
|
// given
|
||||||
|
MultipartFile file = createMultipartFileWithData("77:01:0001001:1234");
|
||||||
|
|
||||||
|
// when
|
||||||
|
String result = fileService.processAsync(file, "test@example.com");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEqualTo("Взято в обработку 1 кадастровых номеров");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveCadastralToGetMissedData_shouldSaveNewCadastrals() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
MissedData existing = new MissedData();
|
||||||
|
existing.setCadastral("77:01:0001001:9999");
|
||||||
|
when(missedDataRepository.findAll()).thenReturn(List.of(existing));
|
||||||
|
|
||||||
|
// when
|
||||||
|
invokeSaveCadastralToGetMissedData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(missedDataRepository).saveAll(anyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveCadastralToGetMissedData_shouldNotSaveExistingCadastrals() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of("77:01:0001001:1234");
|
||||||
|
MissedData existing = new MissedData();
|
||||||
|
existing.setCadastral("77:01:0001001:1234");
|
||||||
|
when(missedDataRepository.findAll()).thenReturn(List.of(existing));
|
||||||
|
|
||||||
|
// when
|
||||||
|
invokeSaveCadastralToGetMissedData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<Iterable<MissedData>> captor = ArgumentCaptor.forClass(Iterable.class);
|
||||||
|
verify(missedDataRepository).saveAll(captor.capture());
|
||||||
|
assertThat(captor.getValue()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
private Workbook createWorkbookWithData(String... values) {
|
||||||
|
Workbook workbook = new XSSFWorkbook();
|
||||||
|
Sheet sheet = workbook.createSheet("Sheet1");
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
Row row = sheet.createRow(i);
|
||||||
|
row.createCell(0).setCellValue(values[i]);
|
||||||
|
}
|
||||||
|
return workbook;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MultipartFile createMultipartFileWithData(String... values) throws IOException {
|
||||||
|
Workbook workbook = createWorkbookWithData(values);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
workbook.write(out);
|
||||||
|
return new MockMultipartFile("file", "test.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", new ByteArrayInputStream(out.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<String> invokeExtractCadastral(Workbook workbook) {
|
||||||
|
try {
|
||||||
|
var method = FileServiceImpl.class.getDeclaredMethod("extractCadastral", Workbook.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (List<String>) method.invoke(fileService, workbook);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void invokeSaveCadastralToGetMissedData(List<String> cadastrals) {
|
||||||
|
try {
|
||||||
|
var method = FileServiceImpl.class.getDeclaredMethod("saveCadastralToGetMissedData", List.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(fileService, cadastrals);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.EnableRetry;
|
||||||
|
import org.springframework.retry.annotation.Retryable;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
@ContextConfiguration(classes = DownloadHeadlessRetryTest.RetryConfig.class)
|
||||||
|
class DownloadHeadlessRetryTest {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableRetry
|
||||||
|
static class RetryConfig {
|
||||||
|
@Bean
|
||||||
|
DownloadHeadlessService downloadHeadlessService() {
|
||||||
|
return new DownloadHeadlessService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DownloadHeadlessService service;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadExcel_shouldBeRetryable() throws Exception {
|
||||||
|
Method method = DownloadHeadlessService.class.getMethod("downloadExcel", String.class);
|
||||||
|
Retryable retryable = method.getAnnotation(Retryable.class);
|
||||||
|
assertThat(retryable).isNotNull();
|
||||||
|
assertThat(retryable.maxAttempts()).isEqualTo(11);
|
||||||
|
assertThat(Arrays.asList(retryable.retryFor())).contains(UncheckedIOException.class);
|
||||||
|
Backoff backoff = retryable.backoff();
|
||||||
|
assertThat(backoff.delay()).isEqualTo(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadExcelFallback_shouldReturnEmptyMap() {
|
||||||
|
Map<String, Lot> result = new DownloadHeadlessService()
|
||||||
|
.downloadExcelFallback(new RuntimeException("boom"), "http://example.com/file.xlsx");
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downloadExcel_shouldRetryAndRecoverToEmptyMap() {
|
||||||
|
// connection refused -> UncheckedIOException on every attempt -> @Recover fallback
|
||||||
|
Map<String, Lot> result = service.downloadExcel("http://127.0.0.1:1/missing.xlsx");
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DownloadHeadlessServiceAdditionalTest {
|
||||||
|
|
||||||
|
private final DownloadHeadlessService service = new DownloadHeadlessService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractMultipleCadastralsWithComma() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234,77:01:0001001:5678";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractConditionalNumber() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Условный номер земельного участка: 77:01:0001001:1234";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldHandleMixedTypes() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234;VIN: X9FKXXEEBMD123456";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234", "X9FKXXEEBMD123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldHandleEmptyParts() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234;;VIN: X9FKXXEEBMD123456";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234", "X9FKXXEEBMD123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldHandleSpaces() {
|
||||||
|
// given
|
||||||
|
String rawValue = " Кадастровый номер : 77:01:0001001:1234 ";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractSingleCadastralWithType() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
|
import com.pledge.torgi.repositories.TorgiEventRepository;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DownloadHeadlessServiceCoverageTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TorgiEventRepository eventRepository;
|
||||||
|
|
||||||
|
private final DownloadHeadlessService service = new DownloadHeadlessService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractData_shouldReturnEmptyForNoCadastrals() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of();
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<Lot> result = invokeExtractData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractData_shouldReturnLotsForCadastral() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of("77:01:0001001:1234");
|
||||||
|
TorgiEvent event = new TorgiEvent();
|
||||||
|
event.setKey("test-key");
|
||||||
|
event.setStatus("PUBLISHED");
|
||||||
|
event.setBidStart("2024-01-01");
|
||||||
|
event.setBidEnd("2024-01-31");
|
||||||
|
event.setTradesDate("2024-02-01");
|
||||||
|
event.setStartPrice("1000000");
|
||||||
|
event.setStep("10000");
|
||||||
|
event.setInitMoney("50000");
|
||||||
|
event.setFinalPrice("1200000");
|
||||||
|
event.setNotice("Notice123");
|
||||||
|
event.setLotNumber(1);
|
||||||
|
event.setCadastral("77:01:0001001:1234");
|
||||||
|
event.setType("Earth");
|
||||||
|
event.setFullInfo("Full info");
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc(anyString())).thenReturn(List.of(event));
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<Lot> result = invokeExtractData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).getStatus()).isEqualTo("PUBLISHED");
|
||||||
|
assertThat(result.get(0).getNotice()).isEqualTo("Notice123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractData_shouldHandleMultipleCadastrals() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
TorgiEvent event1 = new TorgiEvent();
|
||||||
|
event1.setKey("test-key-1");
|
||||||
|
event1.setStatus("PUBLISHED");
|
||||||
|
event1.setCadastral("77:01:0001001:1234");
|
||||||
|
TorgiEvent event2 = new TorgiEvent();
|
||||||
|
event2.setKey("test-key-2");
|
||||||
|
event2.setStatus("CANCELED");
|
||||||
|
event2.setCadastral("77:01:0001001:5678");
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc(anyString())).thenReturn(List.of(event1, event2));
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<Lot> result = invokeExtractData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractData_shouldHandleRepositoryException() {
|
||||||
|
// given
|
||||||
|
List<String> cadastrals = List.of("77:01:0001001:1234");
|
||||||
|
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc(anyString()))
|
||||||
|
.thenThrow(new RuntimeException("Database error"));
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<Lot> result = invokeExtractData(cadastrals);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getFileToSave_shouldReturnFile() {
|
||||||
|
// given
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
|
||||||
|
// when
|
||||||
|
java.io.File result = invokeGetFileToSave(now);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getName()).isEqualTo(now + ".xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setRowHead_shouldCreateHeader() {
|
||||||
|
// given
|
||||||
|
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||||
|
XSSFSheet sheet = workbook.createSheet("Sheet1");
|
||||||
|
|
||||||
|
// when
|
||||||
|
invokeSetRowHead(sheet);
|
||||||
|
|
||||||
|
// then
|
||||||
|
Row headerRow = sheet.getRow(0);
|
||||||
|
assertThat(headerRow).isNotNull();
|
||||||
|
assertThat(headerRow.getCell(0).getStringCellValue()).isEqualTo("Статус торгов");
|
||||||
|
assertThat(headerRow.getCell(1).getStringCellValue()).isEqualTo("Дата начала подачи заявок");
|
||||||
|
assertThat(headerRow.getCell(13).getStringCellValue()).isEqualTo("Время получения события");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createRow_shouldCreateRow() {
|
||||||
|
// given
|
||||||
|
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||||
|
XSSFSheet sheet = workbook.createSheet("Sheet1");
|
||||||
|
Lot lot = new Lot();
|
||||||
|
lot.setStatus("PUBLISHED");
|
||||||
|
lot.setBidStart("2024-01-01");
|
||||||
|
lot.setBidEnd("2024-01-31");
|
||||||
|
lot.setTradesDate("2024-02-01");
|
||||||
|
lot.setStartPrice("1000000");
|
||||||
|
lot.setStep("10000");
|
||||||
|
lot.setInitMoney("50000");
|
||||||
|
lot.setFinalPrice("1200000");
|
||||||
|
lot.setNotice("Notice123");
|
||||||
|
lot.setLotNumber(1);
|
||||||
|
lot.setCadastral("77:01:0001001:1234");
|
||||||
|
lot.setType("Earth");
|
||||||
|
lot.setFullInfo("Full info");
|
||||||
|
|
||||||
|
// when
|
||||||
|
int result = invokeCreateRow(sheet, 0, lot);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEqualTo(1);
|
||||||
|
Row row = sheet.getRow(0);
|
||||||
|
assertThat(row).isNotNull();
|
||||||
|
assertThat(row.getCell(0).getStringCellValue()).isEqualTo("PUBLISHED");
|
||||||
|
assertThat(row.getCell(8).getStringCellValue()).isEqualTo("Notice123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createRow_shouldHandleNullValues() {
|
||||||
|
// given
|
||||||
|
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||||
|
XSSFSheet sheet = workbook.createSheet("Sheet1");
|
||||||
|
Lot lot = new Lot();
|
||||||
|
|
||||||
|
// when
|
||||||
|
int result = invokeCreateRow(sheet, 0, lot);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEqualTo(1);
|
||||||
|
Row row = sheet.getRow(0);
|
||||||
|
assertThat(row).isNotNull();
|
||||||
|
assertThat(row.getCell(0).getStringCellValue()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setRowBodyNew_shouldCreateRows() {
|
||||||
|
// given
|
||||||
|
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||||
|
XSSFSheet sheet = workbook.createSheet("Sheet1");
|
||||||
|
Lot lot1 = new Lot();
|
||||||
|
lot1.setStatus("PUBLISHED");
|
||||||
|
lot1.setNotice("Notice1");
|
||||||
|
Lot lot2 = new Lot();
|
||||||
|
lot2.setStatus("CANCELED");
|
||||||
|
lot2.setNotice("Notice2");
|
||||||
|
List<Lot> lots = List.of(lot1, lot2);
|
||||||
|
|
||||||
|
// when
|
||||||
|
int result = invokeSetRowBodyNew(sheet, 0, lots);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEqualTo(2);
|
||||||
|
assertThat(sheet.getRow(0) != null).isTrue();
|
||||||
|
assertThat(sheet.getRow(1) != null).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Lot> invokeExtractData(List<String> cadastrals) {
|
||||||
|
try {
|
||||||
|
var method = DownloadHeadlessService.class.getDeclaredMethod("extractData", TorgiEventRepository.class, List.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (List<Lot>) method.invoke(service, eventRepository, cadastrals);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.io.File invokeGetFileToSave(LocalDate now) {
|
||||||
|
try {
|
||||||
|
var method = DownloadHeadlessService.class.getDeclaredMethod("getFileToSave", LocalDate.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (java.io.File) method.invoke(service, now);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeSetRowHead(XSSFSheet sheet) {
|
||||||
|
try {
|
||||||
|
var method = DownloadHeadlessService.class.getDeclaredMethod("setRowHead", XSSFSheet.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(service, sheet);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int invokeCreateRow(XSSFSheet sheet, int rowCount, Lot lot) {
|
||||||
|
try {
|
||||||
|
var method = DownloadHeadlessService.class.getDeclaredMethod("createRow", XSSFSheet.class, int.class, Lot.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (int) method.invoke(service, sheet, rowCount, lot);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int invokeSetRowBodyNew(XSSFSheet sheet, int currentRow, List<Lot> lots) {
|
||||||
|
try {
|
||||||
|
var method = DownloadHeadlessService.class.getDeclaredMethod("setRowBodyNew", XSSFSheet.class, int.class, java.util.Collection.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (int) method.invoke(service, sheet, currentRow, lots);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DownloadHeadlessServiceTest {
|
||||||
|
|
||||||
|
private final DownloadHeadlessService service = new DownloadHeadlessService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractSingleCadastral() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractMultipleCadastrals() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Кадастровый номер: 77:01:0001001:1234|77:01:0001001:5678";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("77:01:0001001:1234", "77:01:0001001:5678");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldExtractVIN() {
|
||||||
|
// given
|
||||||
|
String rawValue = "VIN: X9FKXXEEBMD123456";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("X9FKXXEEBMD123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldReturnEmptyForUnknownType() {
|
||||||
|
// given
|
||||||
|
String rawValue = "Неизвестный тип: значение";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractCadastral_shouldReturnEmptyForEmptyInput() {
|
||||||
|
// given
|
||||||
|
String rawValue = "";
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<String> result = service.extractCadastral(rawValue);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/test/java/com/pledge/torgi/service/EmailServiceTest.java
Normal file
167
src/test/java/com/pledge/torgi/service/EmailServiceTest.java
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import com.pledge.torgi.controller.storage.FileService;
|
||||||
|
import com.pledge.torgi.model.Emails;
|
||||||
|
import com.pledge.torgi.repositories.EmailsRepository;
|
||||||
|
import com.pledge.torgi.utils.StartParams;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class EmailServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileService fileService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EmailsRepository emailsRepository;
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
@InjectMocks
|
||||||
|
private EmailService emailService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processEmails_shouldSkipWhenDisabled() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setEnableEmailScan(false);
|
||||||
|
|
||||||
|
// when
|
||||||
|
emailService.processEmails(params);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verifyNoInteractions(emailsRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processEmails_shouldProcessWhenEnabled() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setEnableEmailScan(true);
|
||||||
|
params.setFrom("test@gmail.com");
|
||||||
|
params.setToken("token");
|
||||||
|
params.setSpecialEmail("special@gmail.com");
|
||||||
|
|
||||||
|
when(emailsRepository.findTop10ByOrderByIdDesc()).thenReturn(List.of());
|
||||||
|
|
||||||
|
ReceiveEmailService mockReceiveEmailService = mock(ReceiveEmailService.class);
|
||||||
|
when(mockReceiveEmailService.downloadEmails(any())).thenReturn(List.of());
|
||||||
|
doReturn(mockReceiveEmailService).when(emailService).createReceiveEmailService(any());
|
||||||
|
|
||||||
|
// when
|
||||||
|
emailService.processEmails(params);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(emailsRepository).findTop10ByOrderByIdDesc();
|
||||||
|
verify(mockReceiveEmailService).downloadEmails(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processEmails_shouldProcessNewEmails() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setEnableEmailScan(true);
|
||||||
|
params.setFrom("test@gmail.com");
|
||||||
|
params.setToken("token");
|
||||||
|
params.setSpecialEmail("special@gmail.com");
|
||||||
|
|
||||||
|
Emails processedEmail = new Emails();
|
||||||
|
processedEmail.setMailId("existing-id");
|
||||||
|
when(emailsRepository.findTop10ByOrderByIdDesc()).thenReturn(List.of(processedEmail));
|
||||||
|
|
||||||
|
EmailData newEmailData = new EmailData();
|
||||||
|
newEmailData.setEmailId("new-email-id");
|
||||||
|
newEmailData.setEmails("test@example.com");
|
||||||
|
newEmailData.setFile(new XSSFWorkbook());
|
||||||
|
|
||||||
|
ReceiveEmailService mockReceiveEmailService = mock(ReceiveEmailService.class);
|
||||||
|
when(mockReceiveEmailService.downloadEmails(any())).thenReturn(List.of(newEmailData));
|
||||||
|
doReturn(mockReceiveEmailService).when(emailService).createReceiveEmailService(any());
|
||||||
|
|
||||||
|
when(emailsRepository.existsByMailId("new-email-id")).thenReturn(false);
|
||||||
|
|
||||||
|
// when
|
||||||
|
emailService.processEmails(params);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(emailsRepository).existsByMailId("new-email-id");
|
||||||
|
verify(fileService).processSync(any(), anyString());
|
||||||
|
verify(emailsRepository).save(any(Emails.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processEmails_shouldSkipAlreadyProcessedEmails() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setEnableEmailScan(true);
|
||||||
|
params.setFrom("test@gmail.com");
|
||||||
|
params.setToken("token");
|
||||||
|
params.setSpecialEmail("special@gmail.com");
|
||||||
|
|
||||||
|
Emails processedEmail = new Emails();
|
||||||
|
processedEmail.setMailId("existing-id");
|
||||||
|
when(emailsRepository.findTop10ByOrderByIdDesc()).thenReturn(List.of(processedEmail));
|
||||||
|
|
||||||
|
EmailData existingEmailData = new EmailData();
|
||||||
|
existingEmailData.setEmailId("existing-id");
|
||||||
|
existingEmailData.setEmails("test@example.com");
|
||||||
|
existingEmailData.setFile(new XSSFWorkbook());
|
||||||
|
|
||||||
|
ReceiveEmailService mockReceiveEmailService = mock(ReceiveEmailService.class);
|
||||||
|
when(mockReceiveEmailService.downloadEmails(any())).thenReturn(List.of(existingEmailData));
|
||||||
|
doReturn(mockReceiveEmailService).when(emailService).createReceiveEmailService(any());
|
||||||
|
|
||||||
|
when(emailsRepository.existsByMailId("existing-id")).thenReturn(true);
|
||||||
|
|
||||||
|
// when
|
||||||
|
emailService.processEmails(params);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(emailsRepository).existsByMailId("existing-id");
|
||||||
|
verifyNoInteractions(fileService);
|
||||||
|
verify(emailsRepository, never()).save(any(Emails.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processEmails_shouldSaveProcessedEmail() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
params.setEnableEmailScan(true);
|
||||||
|
params.setFrom("test@gmail.com");
|
||||||
|
params.setToken("token");
|
||||||
|
params.setSpecialEmail("special@gmail.com");
|
||||||
|
|
||||||
|
when(emailsRepository.findTop10ByOrderByIdDesc()).thenReturn(List.of());
|
||||||
|
|
||||||
|
EmailData emailData = new EmailData();
|
||||||
|
emailData.setEmailId("new-id");
|
||||||
|
emailData.setEmails("test@example.com");
|
||||||
|
emailData.setFile(new XSSFWorkbook());
|
||||||
|
|
||||||
|
ReceiveEmailService mockReceiveEmailService = mock(ReceiveEmailService.class);
|
||||||
|
when(mockReceiveEmailService.downloadEmails(any())).thenReturn(List.of(emailData));
|
||||||
|
doReturn(mockReceiveEmailService).when(emailService).createReceiveEmailService(any());
|
||||||
|
|
||||||
|
when(emailsRepository.existsByMailId("new-id")).thenReturn(false);
|
||||||
|
|
||||||
|
// when
|
||||||
|
emailService.processEmails(params);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(emailsRepository).save(argThat(email ->
|
||||||
|
email.getMailId().equals("new-id") &&
|
||||||
|
email.getReplyTo().equals("test@example.com")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/test/java/com/pledge/torgi/service/LotTest.java
Normal file
91
src/test/java/com/pledge/torgi/service/LotTest.java
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import com.pledge.torgi.model.TorgiEvent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class LotTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultConstructor_shouldCreateInstance() {
|
||||||
|
// when
|
||||||
|
Lot lot = new Lot();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(lot).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorFromTorgiEvent_shouldCopyFields() {
|
||||||
|
// given
|
||||||
|
TorgiEvent event = new TorgiEvent();
|
||||||
|
event.setStatus("PUBLISHED");
|
||||||
|
event.setBidStart("2024-01-01");
|
||||||
|
event.setBidEnd("2024-01-31");
|
||||||
|
event.setTradesDate("2024-02-01");
|
||||||
|
event.setStartPrice("1000000");
|
||||||
|
event.setStep("10000");
|
||||||
|
event.setInitMoney("50000");
|
||||||
|
event.setFinalPrice("1200000");
|
||||||
|
event.setNotice("Notice123");
|
||||||
|
event.setLotNumber(1);
|
||||||
|
event.setCadastral("77:01:0001001:1234");
|
||||||
|
event.setType("Earth");
|
||||||
|
event.setFullInfo("Full info");
|
||||||
|
|
||||||
|
// when
|
||||||
|
Lot lot = new Lot(event);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(lot.getStatus()).isEqualTo("PUBLISHED");
|
||||||
|
assertThat(lot.getBidStart()).isEqualTo("2024-01-01");
|
||||||
|
assertThat(lot.getBidEnd()).isEqualTo("2024-01-31");
|
||||||
|
assertThat(lot.getTradesDate()).isEqualTo("2024-02-01");
|
||||||
|
assertThat(lot.getStartPrice()).isEqualTo("1000000");
|
||||||
|
assertThat(lot.getStep()).isEqualTo("10000");
|
||||||
|
assertThat(lot.getInitMoney()).isEqualTo("50000");
|
||||||
|
assertThat(lot.getFinalPrice()).isEqualTo("1200000");
|
||||||
|
assertThat(lot.getNotice()).isEqualTo("Notice123");
|
||||||
|
assertThat(lot.getLotNumber()).isEqualTo(1);
|
||||||
|
assertThat(lot.getCadastral()).isEqualTo("77:01:0001001:1234");
|
||||||
|
assertThat(lot.getType()).isEqualTo("Earth");
|
||||||
|
assertThat(lot.getFullInfo()).isEqualTo("Full info");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settersAndGetters_shouldWork() {
|
||||||
|
// given
|
||||||
|
Lot lot = new Lot();
|
||||||
|
|
||||||
|
// when
|
||||||
|
lot.setStatus("PUBLISHED");
|
||||||
|
lot.setBidStart("2024-01-01");
|
||||||
|
lot.setBidEnd("2024-01-31");
|
||||||
|
lot.setTradesDate("2024-02-01");
|
||||||
|
lot.setStartPrice("1000000");
|
||||||
|
lot.setStep("10000");
|
||||||
|
lot.setInitMoney("50000");
|
||||||
|
lot.setFinalPrice("1200000");
|
||||||
|
lot.setNotice("Notice123");
|
||||||
|
lot.setLotNumber(1);
|
||||||
|
lot.setCadastral("77:01:0001001:1234");
|
||||||
|
lot.setType("Earth");
|
||||||
|
lot.setFullInfo("Full info");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(lot.getStatus()).isEqualTo("PUBLISHED");
|
||||||
|
assertThat(lot.getBidStart()).isEqualTo("2024-01-01");
|
||||||
|
assertThat(lot.getBidEnd()).isEqualTo("2024-01-31");
|
||||||
|
assertThat(lot.getTradesDate()).isEqualTo("2024-02-01");
|
||||||
|
assertThat(lot.getStartPrice()).isEqualTo("1000000");
|
||||||
|
assertThat(lot.getStep()).isEqualTo("10000");
|
||||||
|
assertThat(lot.getInitMoney()).isEqualTo("50000");
|
||||||
|
assertThat(lot.getFinalPrice()).isEqualTo("1200000");
|
||||||
|
assertThat(lot.getNotice()).isEqualTo("Notice123");
|
||||||
|
assertThat(lot.getLotNumber()).isEqualTo(1);
|
||||||
|
assertThat(lot.getCadastral()).isEqualTo("77:01:0001001:1234");
|
||||||
|
assertThat(lot.getType()).isEqualTo("Earth");
|
||||||
|
assertThat(lot.getFullInfo()).isEqualTo("Full info");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.pledge.torgi.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SendEmailServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructor_shouldCreateInstance() {
|
||||||
|
// when
|
||||||
|
SendEmailService service = new SendEmailService("smtp.gmail.com", 587, "user", "pass");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(service).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendMail_shouldThrowExceptionForInvalidFile() {
|
||||||
|
// given
|
||||||
|
SendEmailService service = new SendEmailService("smtp.gmail.com", 587, "user", "pass");
|
||||||
|
File nonExistentFile = new File("non-existent.zip");
|
||||||
|
|
||||||
|
// when & then
|
||||||
|
try {
|
||||||
|
service.sendMail("from@test.com", "to@test.com", "Subject", "Body", nonExistentFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Expected - file doesn't exist
|
||||||
|
assertThat(e).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/test/java/com/pledge/torgi/utils/StartParamsTest.java
Normal file
39
src/test/java/com/pledge/torgi/utils/StartParamsTest.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.pledge.torgi.utils;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class StartParamsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultConstructor_shouldCreateInstance() {
|
||||||
|
// when
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(params).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void settersAndGetters_shouldWork() {
|
||||||
|
// given
|
||||||
|
StartParams params = new StartParams();
|
||||||
|
|
||||||
|
// when
|
||||||
|
params.setFrom("test@gmail.com");
|
||||||
|
params.setToken("token");
|
||||||
|
params.setTo("to@gmail.com");
|
||||||
|
params.setDays("1");
|
||||||
|
params.setSpecialEmail("special@gmail.com");
|
||||||
|
params.setEnableEmailScan(true);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(params.getFrom()).isEqualTo("test@gmail.com");
|
||||||
|
assertThat(params.getToken()).isEqualTo("token");
|
||||||
|
assertThat(params.getTo()).isEqualTo("to@gmail.com");
|
||||||
|
assertThat(params.getDays()).isEqualTo("1");
|
||||||
|
assertThat(params.getSpecialEmail()).isEqualTo("special@gmail.com");
|
||||||
|
assertThat(params.getEnableEmailScan()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user