Speed up upload
This commit is contained in:
parent
a560036265
commit
9dee8a7e84
4 changed files with 714 additions and 145 deletions
|
|
@ -0,0 +1,348 @@
|
|||
package org.operaton.fitpub.service;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.ActivityRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for ActivityPostProcessingService.
|
||||
* Tests async operations in isolation and error handling.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ActivityPostProcessingServiceTest {
|
||||
|
||||
@Mock
|
||||
private PersonalRecordService personalRecordService;
|
||||
|
||||
@Mock
|
||||
private WeatherService weatherService;
|
||||
|
||||
@Mock
|
||||
private HeatmapGridService heatmapGridService;
|
||||
|
||||
@Mock
|
||||
private FederationService federationService;
|
||||
|
||||
@Mock
|
||||
private ActivityImageService activityImageService;
|
||||
|
||||
@Mock
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private ActivityPostProcessingService service;
|
||||
|
||||
private UUID activityId;
|
||||
private UUID userId;
|
||||
private Activity testActivity;
|
||||
private User testUser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
activityId = UUID.randomUUID();
|
||||
userId = UUID.randomUUID();
|
||||
|
||||
// Set baseUrl via reflection (since it's @Value injected)
|
||||
ReflectionTestUtils.setField(service, "baseUrl", "https://test.example");
|
||||
|
||||
// Create test activity
|
||||
testActivity = Activity.builder()
|
||||
.id(activityId)
|
||||
.userId(userId)
|
||||
.activityType(Activity.ActivityType.RUN)
|
||||
.title("Test Run")
|
||||
.description("Morning jog")
|
||||
.visibility(Activity.Visibility.PUBLIC)
|
||||
.totalDistance(BigDecimal.valueOf(5000))
|
||||
.totalDurationSeconds(1800L)
|
||||
.elevationGain(BigDecimal.valueOf(100))
|
||||
.startedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Create test user
|
||||
testUser = User.builder()
|
||||
.id(userId)
|
||||
.username("testrunner")
|
||||
.email("test@example.com")
|
||||
.displayName("Test Runner")
|
||||
.enabled(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully update personal records async")
|
||||
void testUpdatePersonalRecordsAsync_Success() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(personalRecordService.checkAndUpdatePersonalRecords(testActivity)).thenReturn(java.util.List.of());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.updatePersonalRecordsAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Wait for completion
|
||||
verify(activityRepository).findById(activityId);
|
||||
verify(personalRecordService).checkAndUpdatePersonalRecords(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle personal records update failure gracefully")
|
||||
void testUpdatePersonalRecordsAsync_Failure() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
doThrow(new RuntimeException("Database error")).when(personalRecordService).checkAndUpdatePersonalRecords(testActivity);
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.updatePersonalRecordsAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing (error is logged, not propagated)
|
||||
verify(personalRecordService).checkAndUpdatePersonalRecords(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle activity not found in personal records update")
|
||||
void testUpdatePersonalRecordsAsync_ActivityNotFound() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.empty());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.updatePersonalRecordsAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing
|
||||
verify(activityRepository).findById(activityId);
|
||||
verify(personalRecordService, never()).checkAndUpdatePersonalRecords(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully update heatmap async")
|
||||
void testUpdateHeatmapAsync_Success() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
doNothing().when(heatmapGridService).updateHeatmapForActivity(testActivity);
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.updateHeatmapAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(activityRepository).findById(activityId);
|
||||
verify(heatmapGridService).updateHeatmapForActivity(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle heatmap update failure gracefully")
|
||||
void testUpdateHeatmapAsync_Failure() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
doThrow(new RuntimeException("Heatmap error")).when(heatmapGridService).updateHeatmapForActivity(testActivity);
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.updateHeatmapAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing
|
||||
verify(heatmapGridService).updateHeatmapForActivity(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully fetch weather async")
|
||||
void testFetchWeatherAsync_Success() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(weatherService.fetchWeatherForActivity(testActivity)).thenReturn(Optional.empty());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.fetchWeatherAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(activityRepository).findById(activityId);
|
||||
verify(weatherService).fetchWeatherForActivity(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle weather fetch failure gracefully")
|
||||
void testFetchWeatherAsync_Failure() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
doThrow(new RuntimeException("Weather API error")).when(weatherService).fetchWeatherForActivity(testActivity);
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.fetchWeatherAsync(activityId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing
|
||||
verify(weatherService).fetchWeatherForActivity(testActivity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully publish to federation async for PUBLIC activity")
|
||||
void testPublishToFederationAsync_PublicActivity() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
testActivity.setVisibility(Activity.Visibility.PUBLIC);
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
when(activityImageService.generateActivityImage(testActivity)).thenReturn("https://test.example/image.png");
|
||||
doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(activityRepository).findById(activityId);
|
||||
verify(userRepository).findById(userId);
|
||||
verify(activityImageService).generateActivityImage(testActivity);
|
||||
verify(federationService).sendCreateActivity(anyString(), any(), eq(testUser), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully publish to federation async for FOLLOWERS activity")
|
||||
void testPublishToFederationAsync_FollowersActivity() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
testActivity.setVisibility(Activity.Visibility.FOLLOWERS);
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
when(activityImageService.generateActivityImage(testActivity)).thenReturn("https://test.example/image.png");
|
||||
doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(federationService).sendCreateActivity(anyString(), any(), eq(testUser), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should skip federation for PRIVATE activity")
|
||||
void testPublishToFederationAsync_PrivateActivity() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
testActivity.setVisibility(Activity.Visibility.PRIVATE);
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(activityImageService, never()).generateActivityImage(any());
|
||||
verify(federationService, never()).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle federation publish failure gracefully")
|
||||
void testPublishToFederationAsync_Failure() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
testActivity.setVisibility(Activity.Visibility.PUBLIC);
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
when(activityImageService.generateActivityImage(testActivity)).thenReturn("https://test.example/image.png");
|
||||
doThrow(new RuntimeException("Federation error")).when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing
|
||||
verify(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle image generation failure and continue with federation")
|
||||
void testPublishToFederationAsync_ImageGenerationFailure() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
testActivity.setVisibility(Activity.Visibility.PUBLIC);
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
when(activityImageService.generateActivityImage(testActivity)).thenThrow(new RuntimeException("Image generation failed"));
|
||||
doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get();
|
||||
verify(activityImageService).generateActivityImage(testActivity);
|
||||
verify(federationService).sendCreateActivity(anyString(), any(), eq(testUser), eq(true)); // Should still publish without image
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle user not found in federation publish")
|
||||
void testPublishToFederationAsync_UserNotFound() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.empty());
|
||||
|
||||
// When
|
||||
CompletableFuture<Void> future = service.publishToFederationAsync(activityId, userId);
|
||||
|
||||
// Then
|
||||
assertNotNull(future);
|
||||
future.get(); // Should complete without throwing
|
||||
verify(userRepository).findById(userId);
|
||||
verify(federationService, never()).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should format activity content correctly")
|
||||
void testFormatActivityContent() {
|
||||
// Given: Activity with all metrics set in setUp()
|
||||
|
||||
// When: Call processActivityAsync which will use formatActivityContent internally
|
||||
when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
|
||||
when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
|
||||
when(activityImageService.generateActivityImage(testActivity)).thenReturn(null);
|
||||
doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
|
||||
// When
|
||||
try {
|
||||
service.publishToFederationAsync(activityId, userId).get();
|
||||
} catch (Exception e) {
|
||||
fail("Should not throw exception");
|
||||
}
|
||||
|
||||
// Then: Verify federation was called (content formatting is tested indirectly)
|
||||
verify(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue