Add Docker build and deploy pipeline
All checks were successful
deploy-backend / deploy (push) Successful in 8m24s
All checks were successful
deploy-backend / deploy (push) Successful in 8m24s
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
.gradle
|
||||
build
|
||||
target
|
||||
.idea
|
||||
39
.env.example
Normal file
39
.env.example
Normal file
@@ -0,0 +1,39 @@
|
||||
# Шаблон переменных окружения. Скопируй в .env и заполни значениями.
|
||||
# Сам .env добавлен в .gitignore и не должен попадать в репозиторий.
|
||||
#
|
||||
# Как приложение их получает:
|
||||
# docker compose читает .env автоматически;
|
||||
# Spring Boot — нет. Нужно экспортировать переменные в терминал
|
||||
# или прописать их в Environment variables в настройках запуска IDE.
|
||||
|
||||
# --- Обязательные: без них приложение не стартует ---
|
||||
|
||||
# Ключи доступа к MinIO (S3-совместимое хранилище).
|
||||
# ВНИМАНИЕ: они должны совпадать с MINIO_ACCESS_KEY / MINIO_SECRET_KEY
|
||||
# в docker-compose.yaml, иначе приложение не подключится к MinIO.
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
|
||||
# Пароль Redis. Тот же, что в docker-compose.yaml.
|
||||
# docker-compose падает с понятной ошибкой, если переменная пустая.
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# --- Рекомендуется задать ---
|
||||
|
||||
# Секрет подписи JWT. Без переменной берётся захардкоженное значение
|
||||
# из application.yaml — подделать такой токен может кто угодно.
|
||||
# Сгенерировать, например, так:
|
||||
# openssl rand -base64 48
|
||||
JWT_SECRET=
|
||||
|
||||
# --- Необязательно: есть значения по умолчанию ---
|
||||
|
||||
# REDIS_HOST=localhost
|
||||
# MINIO_ENDPOINT=http://localhost:9010
|
||||
# MINIO_BUCKET=refound-images
|
||||
# MINIO_ADS_BUCKET=ads-media
|
||||
# MINIO_REGION=us-east-1
|
||||
# AI_SERVICE_URL=http://localhost:8000
|
||||
# JWT_ACCESS_EXPIRATION=900000
|
||||
# JWT_REFRESH_EXPIRATION=604800000
|
||||
# JPA_DDL_AUTO=validate
|
||||
16
.gitea/workflows/deploy.yml
Normal file
16
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
name: deploy-backend
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: host
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build image
|
||||
run: docker build -t nakhodka-backend:latest -t nakhodka-backend:${{ github.sha }} .
|
||||
- name: Restart backend
|
||||
run: docker compose -f /opt/nakhodka/docker-compose.yml up -d backend
|
||||
- name: Cleanup
|
||||
run: docker image prune -f
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM eclipse-temurin:25-jdk AS build
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN sed -i 's/\r$//' gradlew \
|
||||
&& chmod +x gradlew \
|
||||
&& ./gradlew bootJar --no-daemon -x test \
|
||||
&& cp "$(ls build/libs/*.jar | grep -v plain | head -n1)" /app/app.jar
|
||||
|
||||
FROM eclipse-temurin:25-jre
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd -r -u 1001 app
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/app.jar app.jar
|
||||
USER app
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-Xmx768m", "-jar", "app.jar"]
|
||||
@@ -1,5 +1,5 @@
|
||||
ffmpeg:
|
||||
binary-path: C:\ffmpeg\ffmpeg-9.0.2-essentials_build\bin\ffmpeg.exe
|
||||
binary-path: ${FFMPEG_PATH:C:\ffmpeg\ffmpeg-9.0.2-essentials_build\bin\ffmpeg.exe}
|
||||
|
||||
spring:
|
||||
servlet:
|
||||
@@ -8,9 +8,9 @@ spring:
|
||||
max-request-size: 20MB
|
||||
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/nahodka
|
||||
username: user
|
||||
password: pass
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/nahodka}
|
||||
username: ${DB_USER:user}
|
||||
password: ${DB_PASSWORD:pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
jpa:
|
||||
@@ -19,7 +19,7 @@ spring:
|
||||
# из-за чего базы расходились. Для разработки можно переопределить
|
||||
# переменной окружения JPA_DDL_AUTO=update.
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: true
|
||||
show-sql: ${JPA_SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
@@ -55,10 +55,10 @@ ai:
|
||||
multiplier: 2
|
||||
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:mySuperSecretKeyForJwtTokenGenerationThatIsLongEnough2024!ReFound}
|
||||
secret: ${JWT_SECRET}
|
||||
access-expiration: ${JWT_ACCESS_EXPIRATION:900000} # 15 минут
|
||||
refresh-expiration: ${JWT_REFRESH_EXPIRATION:604800000} # 7 дней
|
||||
|
||||
app:
|
||||
cors:
|
||||
allowed-origins: http://localhost:5173,http://localhost:3000,http://192.168.1.76:3000
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000,http://192.168.1.76:3000}
|
||||
@@ -0,0 +1,377 @@
|
||||
package com.krylov.refound.service.admin;
|
||||
|
||||
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.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.krylov.refound.dto.AddressInfo;
|
||||
import com.krylov.refound.dto.admin.AdminPostResponse;
|
||||
import com.krylov.refound.entity.Image;
|
||||
import com.krylov.refound.entity.Post;
|
||||
import com.krylov.refound.entity.User;
|
||||
import com.krylov.refound.enums.PostCategory;
|
||||
import com.krylov.refound.enums.PostStatus;
|
||||
import com.krylov.refound.enums.PostType;
|
||||
import com.krylov.refound.exception.ApiException;
|
||||
import com.krylov.refound.repository.ChatRepository;
|
||||
import com.krylov.refound.repository.FavoriteRepository;
|
||||
import com.krylov.refound.repository.ImageRepository;
|
||||
import com.krylov.refound.repository.PostRepository;
|
||||
import com.krylov.refound.service.FileStorageService;
|
||||
import com.krylov.refound.service.GeocodingService;
|
||||
import com.krylov.refound.service.StatisticsService;
|
||||
import com.krylov.refound.service.redis.MapCacheService;
|
||||
import com.krylov.refound.service.redis.PostCacheService;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminPostServiceTest {
|
||||
|
||||
@Mock
|
||||
private PostRepository postRepository;
|
||||
@Mock
|
||||
private ImageRepository imageRepository;
|
||||
@Mock
|
||||
private FavoriteRepository favoriteRepository;
|
||||
@Mock
|
||||
private ChatRepository chatRepository;
|
||||
@Mock
|
||||
private FileStorageService fileStorageService;
|
||||
@Mock
|
||||
private GeocodingService geocodingService;
|
||||
@Mock
|
||||
private PostCacheService postCacheService;
|
||||
@Mock
|
||||
private MapCacheService mapCacheService;
|
||||
@Mock
|
||||
private StatisticsService statisticsService;
|
||||
|
||||
@Spy
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@InjectMocks
|
||||
private AdminPostService service;
|
||||
|
||||
private static Post post(Long id, PostStatus status) {
|
||||
User owner = new User();
|
||||
owner.setId(7L);
|
||||
owner.setLogin("ivan");
|
||||
owner.setName("Иван");
|
||||
owner.setLastName("Петров");
|
||||
|
||||
Post post = new Post();
|
||||
post.setId(id);
|
||||
post.setStatus(status);
|
||||
post.setType(PostType.LOSS);
|
||||
post.setTitle("Кот");
|
||||
post.setCategory(PostCategory.ANIMAL);
|
||||
post.setCity("Москва");
|
||||
post.setUser(owner);
|
||||
return post;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPosts_fillsOwnerAndImages() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
Image image = new Image();
|
||||
image.setId(5L);
|
||||
image.setUrl("images/cat.png");
|
||||
image.setPost(post);
|
||||
|
||||
when(postRepository.findAll(any(Specification.class), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(post)));
|
||||
when(imageRepository.findByPostIdIn(anyCollection())).thenReturn(List.of(image));
|
||||
|
||||
List<AdminPostResponse> content = service
|
||||
.getPosts(null, null, null, PageRequest.of(0, 20))
|
||||
.getContent();
|
||||
|
||||
assertThat(content).hasSize(1);
|
||||
AdminPostResponse dto = content.get(0);
|
||||
assertThat(dto.getOwnerLogin()).isEqualTo("ivan");
|
||||
assertThat(dto.getOwnerFirstName()).isEqualTo("Иван");
|
||||
assertThat(dto.getOwnerLastName()).isEqualTo("Петров");
|
||||
assertThat(dto.getImages()).containsExactly("images/cat.png");
|
||||
assertThat(dto.getStatus()).isEqualTo(PostStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPosts_rejectsUnknownFilterValue() {
|
||||
assertThatThrownBy(() -> service.getPosts(null, "WRONG", null, PageRequest.of(0, 20)))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.satisfies(ex -> assertThat(((ApiException) ex).getStatus()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPosts_clampsPageSize() {
|
||||
when(postRepository.findAll(any(Specification.class), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
service.getPosts(null, null, null, PageRequest.of(0, 5000));
|
||||
|
||||
ArgumentCaptor<Pageable> captor = ArgumentCaptor.forClass(Pageable.class);
|
||||
verify(postRepository).findAll(any(Specification.class), captor.capture());
|
||||
assertThat(captor.getValue().getPageSize()).isEqualTo(100);
|
||||
assertThat(captor.getValue().getSort().getOrderFor("createdAt")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPosts_limitsStatusesToActiveAndClosed() {
|
||||
assertThat(statusesUsedByGetPosts()).containsExactlyInAnyOrder(PostStatus.ACTIVE, PostStatus.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRejectedPosts_limitsStatusesToRejected() {
|
||||
assertThat(statusesUsedByGetRejectedPosts()).containsExactly(PostStatus.REJECTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void feedsRejectStatusFromAnotherFeed() {
|
||||
assertThatThrownBy(() -> service.getPosts(null, null, "REJECTED", PageRequest.of(0, 20)))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.satisfies(ex -> assertThat(((ApiException) ex).getStatus()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
assertThatThrownBy(() -> service.getRejectedPosts(null, null, "ACTIVE", PageRequest.of(0, 20)))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.satisfies(ex -> assertThat(((ApiException) ex).getStatus()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
verify(postRepository, never()).findAll(any(Specification.class), any(Pageable.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRejectedPosts_returnsRejectedPostsWithOwnerAndImages() {
|
||||
Post post = post(1L, PostStatus.REJECTED);
|
||||
Image image = new Image();
|
||||
image.setUrl("images/cat.png");
|
||||
image.setPost(post);
|
||||
|
||||
when(postRepository.findAll(any(Specification.class), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(post)));
|
||||
when(imageRepository.findByPostIdIn(anyCollection())).thenReturn(List.of(image));
|
||||
|
||||
List<AdminPostResponse> content = service
|
||||
.getRejectedPosts(null, null, null, PageRequest.of(0, 20))
|
||||
.getContent();
|
||||
|
||||
assertThat(content).hasSize(1);
|
||||
assertThat(content.get(0).getStatus()).isEqualTo(PostStatus.REJECTED);
|
||||
assertThat(content.get(0).getOwnerLogin()).isEqualTo("ivan");
|
||||
assertThat(content.get(0).getImages()).containsExactly("images/cat.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* Статусы достаются из спецификации, которую сервис отдал в репозиторий: сам SQL
|
||||
* без БД не проверить, а IN(...) собирается ровно из переданного allow-list.
|
||||
*/
|
||||
private Set<PostStatus> statusesUsedByGetPosts() {
|
||||
when(postRepository.findAll(any(Specification.class), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
service.getPosts(null, null, null, PageRequest.of(0, 20));
|
||||
return capturedStatuses();
|
||||
}
|
||||
|
||||
private Set<PostStatus> statusesUsedByGetRejectedPosts() {
|
||||
when(postRepository.findAll(any(Specification.class), any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
service.getRejectedPosts(null, null, null, PageRequest.of(0, 20));
|
||||
return capturedStatuses();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<PostStatus> capturedStatuses() {
|
||||
ArgumentCaptor<Specification<Post>> specCaptor = ArgumentCaptor.forClass(Specification.class);
|
||||
verify(postRepository).findAll(specCaptor.capture(), any(Pageable.class));
|
||||
|
||||
Set<PostStatus> statuses = EnumSet.noneOf(PostStatus.class);
|
||||
// Hibernate отдаёт из Path.in() объект, который ещё и Predicate, — в тесте так же.
|
||||
Path<Object> path = (Path<Object>) mock(Path.class, withSettings().extraInterfaces(Predicate.class));
|
||||
Expression<Integer> inExpression = (Expression<Integer>) (Expression<?>) path;
|
||||
Root<Post> root = mock(Root.class);
|
||||
// deep stubs: Specification.allOf комбинирует предикаты через CriteriaBuilder.and()
|
||||
CriteriaBuilder cb = mock(CriteriaBuilder.class, RETURNS_DEEP_STUBS);
|
||||
|
||||
when(root.<Object>get(anyString())).thenReturn(path);
|
||||
when(path.in(any(Collection.class))).thenAnswer(invocation -> {
|
||||
statuses.addAll((Collection<PostStatus>) invocation.getArgument(0));
|
||||
return inExpression;
|
||||
});
|
||||
|
||||
specCaptor.getValue().toPredicate(root, mock(CriteriaQuery.class), cb);
|
||||
return statuses;
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePost_keepsStatusAndInvalidatesCache() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
when(postRepository.save(any(Post.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
AdminPostResponse dto = service.updatePost(1L, "FOUND", "Найден кот", "Москва",
|
||||
null, null, "описание", null, "+7", true, "100", List.of(), "[]");
|
||||
|
||||
assertThat(dto.getStatus()).isEqualTo(PostStatus.ACTIVE);
|
||||
assertThat(post.getStatus()).isEqualTo(PostStatus.ACTIVE);
|
||||
assertThat(post.getType()).isEqualTo(PostType.FOUND);
|
||||
assertThat(post.getCity()).isEqualTo("Москва");
|
||||
assertThat(post.getIsReward()).isTrue();
|
||||
verify(postCacheService).invalidatePosts();
|
||||
verify(mapCacheService).clearMapMarkersCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePost_dropsImagesMissingFromExistingList() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
Image kept = new Image();
|
||||
kept.setUrl("images/keep.png");
|
||||
kept.setPost(post);
|
||||
Image dropped = new Image();
|
||||
dropped.setUrl("images/drop.png");
|
||||
dropped.setPost(post);
|
||||
post.getImages().addAll(List.of(kept, dropped));
|
||||
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
when(postRepository.save(any(Post.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
service.updatePost(1L, "LOSS", "Кот", "Москва", null, null, null, null, null,
|
||||
false, null, List.of(), "[\"images/keep.png\"]");
|
||||
|
||||
assertThat(post.getImages()).extracting(Image::getUrl).containsExactly("images/keep.png");
|
||||
verify(fileStorageService).deleteFile("images/drop.png");
|
||||
verify(fileStorageService, never()).deleteFile("images/keep.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePost_fallsBackToAddressWhenGeocodingUnavailable() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
when(postRepository.save(any(Post.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
when(geocodingService.getAddress("55.0", "37.0"))
|
||||
.thenThrow(new ApiException(com.krylov.refound.enums.ErrorCode.INTERNAL_ERROR,
|
||||
"Сервис геокодирования недоступен.", HttpStatus.SERVICE_UNAVAILABLE));
|
||||
|
||||
service.updatePost(1L, "LOSS", "Кот", "Тверь", 55.0, 37.0, null, null, null,
|
||||
false, null, List.of(), "[]");
|
||||
|
||||
assertThat(post.getCity()).isEqualTo("Тверь");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateStatus_closesPostAndIncrementsStatistics() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
when(postRepository.save(any(Post.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
AdminPostResponse dto = service.updateStatus(1L, "CLOSED");
|
||||
|
||||
assertThat(dto.getStatus()).isEqualTo(PostStatus.CLOSED);
|
||||
verify(statisticsService).incrementFound();
|
||||
verify(postCacheService).invalidatePosts();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateStatus_reopeningDoesNotTouchStatistics() {
|
||||
Post post = post(1L, PostStatus.CLOSED);
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
when(postRepository.save(any(Post.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
AdminPostResponse dto = service.updateStatus(1L, "active");
|
||||
|
||||
assertThat(dto.getStatus()).isEqualTo(PostStatus.ACTIVE);
|
||||
verify(statisticsService, never()).incrementFound();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateStatus_sameStatusIsNoop() {
|
||||
Post post = post(1L, PostStatus.ACTIVE);
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
|
||||
AdminPostResponse dto = service.updateStatus(1L, "ACTIVE");
|
||||
|
||||
assertThat(dto.getStatus()).isEqualTo(PostStatus.ACTIVE);
|
||||
verify(postRepository, never()).save(any(Post.class));
|
||||
verify(statisticsService, never()).incrementFound();
|
||||
verify(postCacheService, never()).invalidatePosts();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateStatus_rejectsGarbage() {
|
||||
assertThatThrownBy(() -> service.updateStatus(1L, "banana"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.satisfies(ex -> assertThat(((ApiException) ex).getStatus()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePost_throwsNotFoundForMissingPost() {
|
||||
when(postRepository.findById(99L)).thenReturn(java.util.Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> service.updatePost(99L, "LOSS", "t", null, null, null, null, null,
|
||||
null, false, null, List.of(), "[]"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.satisfies(ex -> assertThat(((ApiException) ex).getStatus()).isEqualTo(HttpStatus.NOT_FOUND));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletePost_clearsFilesFavoritesAndChatLinks() {
|
||||
Post post = post(1L, PostStatus.CLOSED);
|
||||
Image image = new Image();
|
||||
image.setUrl("images/cat.png");
|
||||
image.setPost(post);
|
||||
post.getImages().add(image);
|
||||
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
|
||||
service.deletePost(1L);
|
||||
|
||||
verify(fileStorageService).deleteFile("images/cat.png");
|
||||
verify(favoriteRepository).deleteByPostIn(anyList());
|
||||
verify(chatRepository).clearPostReference(1L);
|
||||
verify(postRepository).delete(post);
|
||||
verify(postCacheService).invalidatePosts();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletePost_doesNotFailWhenFileStorageIsDown() {
|
||||
Post post = post(1L, PostStatus.CLOSED);
|
||||
Image image = new Image();
|
||||
image.setUrl("images/cat.png");
|
||||
image.setPost(post);
|
||||
post.getImages().add(image);
|
||||
|
||||
when(postRepository.findById(1L)).thenReturn(java.util.Optional.of(post));
|
||||
org.mockito.Mockito.doThrow(new RuntimeException("MinIO недоступен"))
|
||||
.when(fileStorageService).deleteFile(any());
|
||||
|
||||
service.deletePost(1L);
|
||||
|
||||
verify(postRepository).delete(post);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user