Compare commits
6 Commits
5e77a63aff
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd62fba75 | ||
|
|
76aa1ab253 | ||
|
|
7438e2b57a | ||
|
|
7ad5361a61 | ||
|
|
e9204516cf | ||
|
|
a4067202ec |
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"
|
||||||
@@ -14,6 +14,9 @@ services:
|
|||||||
- 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
|
||||||
@@ -58,6 +61,6 @@ services:
|
|||||||
- client-backend
|
- client-backend
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
networks:
|
||||||
db-data:
|
monitoring:
|
||||||
pgadmin-data:
|
external: true
|
||||||
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
|
||||||
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
4
pom.xml
4
pom.xml
@@ -88,6 +88,10 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>2.8.17</version>
|
<version>2.8.17</version>
|
||||||
</dependency>
|
</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>
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ 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 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;
|
||||||
@@ -17,8 +19,10 @@ 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.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;
|
||||||
|
|
||||||
@@ -34,12 +38,21 @@ public class DownloadHeadlessController {
|
|||||||
private final TorgiEventRepository eventRepository;
|
private final TorgiEventRepository eventRepository;
|
||||||
private final MissedDataRepository missedDataRepository;
|
private final MissedDataRepository missedDataRepository;
|
||||||
private final DownloadHeadlessService downloadHeadlessService;
|
private final DownloadHeadlessService downloadHeadlessService;
|
||||||
|
private final Counter diffAdded;
|
||||||
|
private final Counter missedAdded;
|
||||||
|
private final AtomicLong lastPollTimestamp;
|
||||||
|
|
||||||
public DownloadHeadlessController(TorgiEventRepository eventRepository, MissedDataRepository missedDataRepository,
|
public DownloadHeadlessController(TorgiEventRepository eventRepository, MissedDataRepository missedDataRepository,
|
||||||
DownloadHeadlessService downloadHeadlessService) {
|
DownloadHeadlessService downloadHeadlessService, MeterRegistry meterRegistry) {
|
||||||
this.eventRepository = eventRepository;
|
this.eventRepository = eventRepository;
|
||||||
this.missedDataRepository = missedDataRepository;
|
this.missedDataRepository = missedDataRepository;
|
||||||
this.downloadHeadlessService = downloadHeadlessService;
|
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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +70,11 @@ public class DownloadHeadlessController {
|
|||||||
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
||||||
content.add(key);
|
content.add(key);
|
||||||
businessLogger.info("added new diff content");
|
businessLogger.info("added new diff content");
|
||||||
|
diffAdded.increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRepository.saveAll(newEvents);
|
eventRepository.saveAll(newEvents);
|
||||||
|
lastPollTimestamp.set(Instant.now().getEpochSecond());
|
||||||
businessLogger.info("completeProcessing readed from torgi = {}, added to DB = {}", currentContent.size(), newEvents.size());
|
businessLogger.info("completeProcessing readed from torgi = {}, added to DB = {}", currentContent.size(), newEvents.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +171,7 @@ public class DownloadHeadlessController {
|
|||||||
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
newEvents.add(new TorgiEvent(key, currentContent.get(key)));
|
||||||
content.add(key);
|
content.add(key);
|
||||||
businessLogger.info("added new missed content");
|
businessLogger.info("added new missed content");
|
||||||
|
missedAdded.increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRepository.saveAll(newEvents);
|
eventRepository.saveAll(newEvents);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ 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 io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -76,7 +77,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
.thenReturn(page);
|
.thenReturn(page);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
verify(eventRepository).findAllByOrderByCreatedAtDesc(any(PageRequest.class));
|
verify(eventRepository).findAllByOrderByCreatedAtDesc(any(PageRequest.class));
|
||||||
@@ -93,7 +94,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
.thenReturn(new PageImpl<>(List.of(event1, event2)));
|
.thenReturn(new PageImpl<>(List.of(event1, event2)));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(getContent(controller)).containsExactlyInAnyOrder("key-1", "key-2");
|
assertThat(getContent(controller)).containsExactlyInAnyOrder("key-1", "key-2");
|
||||||
@@ -110,7 +111,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
lot.setFullInfo("Full info");
|
lot.setFullInfo("Full info");
|
||||||
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.process();
|
controller.process();
|
||||||
@@ -138,7 +139,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
.thenReturn(new PageImpl<>(List.of(existing)));
|
.thenReturn(new PageImpl<>(List.of(existing)));
|
||||||
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.process();
|
controller.process();
|
||||||
@@ -159,7 +160,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
List<String> cadastral = List.of(CADASTRAL);
|
List<String> cadastral = List.of(CADASTRAL);
|
||||||
|
|
||||||
try (MockedConstruction<SendEmailService> emailConstruction = mockConstruction(SendEmailService.class)) {
|
try (MockedConstruction<SendEmailService> emailConstruction = mockConstruction(SendEmailService.class)) {
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.sendNewContent(params, cadastral, "to@example.com");
|
controller.sendNewContent(params, cadastral, "to@example.com");
|
||||||
@@ -186,7 +187,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
StartParams params = new StartParams();
|
StartParams params = new StartParams();
|
||||||
params.setFrom("from@example.com");
|
params.setFrom("from@example.com");
|
||||||
params.setToken("token");
|
params.setToken("token");
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
try (MockedConstruction<SendEmailService> ignored = mockConstruction(SendEmailService.class,
|
try (MockedConstruction<SendEmailService> ignored = mockConstruction(SendEmailService.class,
|
||||||
(mock, context) -> doThrow(new RuntimeException("smtp failure"))
|
(mock, context) -> doThrow(new RuntimeException("smtp failure"))
|
||||||
@@ -215,7 +216,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
lot.setCadastral(CADASTRAL);
|
lot.setCadastral(CADASTRAL);
|
||||||
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.richData();
|
controller.richData();
|
||||||
@@ -241,7 +242,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
|
|
||||||
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.richData();
|
controller.richData();
|
||||||
@@ -260,7 +261,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
missed.setCadastral(CADASTRAL);
|
missed.setCadastral(CADASTRAL);
|
||||||
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(Collections.emptyMap());
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(Collections.emptyMap());
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.richData();
|
controller.richData();
|
||||||
@@ -281,7 +282,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
|
|
||||||
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
|
||||||
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
controller.richData();
|
controller.richData();
|
||||||
@@ -295,7 +296,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
void richData_shouldHandleExceptionGracefully() {
|
void richData_shouldHandleExceptionGracefully() {
|
||||||
// given
|
// given
|
||||||
when(missedDataRepository.findAll(any(PageRequest.class))).thenThrow(new RuntimeException("Database error"));
|
when(missedDataRepository.findAll(any(PageRequest.class))).thenThrow(new RuntimeException("Database error"));
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when / then
|
// when / then
|
||||||
assertThatCode(controller::richData).doesNotThrowAnyException();
|
assertThatCode(controller::richData).doesNotThrowAnyException();
|
||||||
@@ -308,7 +309,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
File deeper = new File(nested, "deeper");
|
File deeper = new File(nested, "deeper");
|
||||||
assertThat(deeper.mkdirs()).isTrue();
|
assertThat(deeper.mkdirs()).isTrue();
|
||||||
writeFile(new File(deeper, "file.txt"), "content");
|
writeFile(new File(deeper, "file.txt"), "content");
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
boolean result = invokeDeleteDirectory(controller, nested);
|
boolean result = invokeDeleteDirectory(controller, nested);
|
||||||
@@ -371,7 +372,7 @@ class DownloadHeadlessControllerTest {
|
|||||||
File nested = new File(sourceDir, "nested");
|
File nested = new File(sourceDir, "nested");
|
||||||
assertThat(nested.mkdirs()).isTrue();
|
assertThat(nested.mkdirs()).isTrue();
|
||||||
writeFile(new File(nested, "data.txt"), "archive-content");
|
writeFile(new File(nested, "data.txt"), "archive-content");
|
||||||
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
|
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService, new SimpleMeterRegistry());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// when
|
// when
|
||||||
@@ -388,6 +389,27 @@ class DownloadHeadlessControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
// Helpers
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|||||||
@@ -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,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user