feat(back-end): admin home edit image page

This commit is contained in:
2026-03-09 17:44:17 +01:00
parent 9e306ea1d1
commit 17df0c6b9b
25 changed files with 2634 additions and 103 deletions

View File

@@ -100,6 +100,7 @@ class AdminMediaControllerServiceTest {
);
when(clamAVService.scan(any())).thenReturn(true);
when(mediaFfmpegService.canEncode(anyString())).thenReturn(true);
when(mediaAssetRepository.save(any(MediaAsset.class))).thenAnswer(invocation -> {
MediaAsset asset = invocation.getArgument(0);
@@ -246,6 +247,33 @@ class AdminMediaControllerServiceTest {
assertTrue(variants.isEmpty());
}
@Test
void uploadAsset_withLimitedEncoders_shouldKeepAssetReadyAndExposeOnlySupportedVariants() throws Exception {
when(mediaImageInspector.inspect(any(Path.class))).thenReturn(
new MediaImageInspector.ImageMetadata("image/jpeg", "jpg", 1200, 800)
);
when(mediaFfmpegService.canEncode("JPEG")).thenReturn(true);
when(mediaFfmpegService.canEncode("WEBP")).thenReturn(false);
when(mediaFfmpegService.canEncode("AVIF")).thenReturn(false);
MockMultipartFile file = new MockMultipartFile(
"file",
"capability.jpg",
"image/jpeg",
"jpeg-image-content".getBytes(StandardCharsets.UTF_8)
);
AdminMediaAssetDto dto = service.uploadAsset(file, "Capability", null, "PUBLIC");
assertEquals("READY", dto.getStatus());
assertEquals(4, dto.getVariants().size());
assertEquals(3, dto.getVariants().stream()
.filter(variant -> "JPEG".equals(variant.getFormat()))
.count());
assertTrue(dto.getVariants().stream()
.noneMatch(variant -> "WEBP".equals(variant.getFormat()) || "AVIF".equals(variant.getFormat())));
}
@Test
void uploadAsset_withOversizedFile_shouldFailValidationBeforePersistence() {
service = new AdminMediaControllerService(

View File

@@ -0,0 +1,142 @@
package com.printcalculator.service.media;
import com.printcalculator.dto.PublicMediaUsageDto;
import com.printcalculator.entity.MediaAsset;
import com.printcalculator.entity.MediaUsage;
import com.printcalculator.entity.MediaVariant;
import com.printcalculator.repository.MediaUsageRepository;
import com.printcalculator.repository.MediaVariantRepository;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PublicMediaQueryServiceTest {
@Mock
private MediaUsageRepository mediaUsageRepository;
@Mock
private MediaVariantRepository mediaVariantRepository;
@TempDir
Path tempDir;
private PublicMediaQueryService service;
@BeforeEach
void setUp() {
MediaStorageService mediaStorageService = new MediaStorageService(
tempDir.resolve("storage_media").toString(),
"https://cdn.example/media"
);
service = new PublicMediaQueryService(mediaUsageRepository, mediaVariantRepository, mediaStorageService);
}
@Test
void getUsageMedia_shouldReturnOnlyActiveReadyPublicUsagesOrderedBySortOrder() {
MediaAsset readyPublicAsset = buildAsset("READY", "PUBLIC", "Shop hero", "Shop alt");
MediaAsset draftAsset = buildAsset("PROCESSING", "PUBLIC", "Draft", "Draft alt");
MediaAsset privateAsset = buildAsset("READY", "PRIVATE", "Private", "Private alt");
MediaUsage usageSecond = buildUsage(readyPublicAsset, "HOME_SECTION", "shop-gallery", 2, false, true);
MediaUsage usageFirst = buildUsage(readyPublicAsset, "HOME_SECTION", "shop-gallery", 1, true, true);
MediaUsage usageDraft = buildUsage(draftAsset, "HOME_SECTION", "shop-gallery", 0, false, true);
MediaUsage usagePrivate = buildUsage(privateAsset, "HOME_SECTION", "shop-gallery", 3, false, true);
when(mediaUsageRepository.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
"HOME_SECTION", "shop-gallery"
)).thenReturn(List.of(usageSecond, usageFirst, usageDraft, usagePrivate));
when(mediaVariantRepository.findByMediaAsset_IdIn(List.of(readyPublicAsset.getId())))
.thenReturn(List.of(
buildVariant(readyPublicAsset, "thumb", "JPEG", "asset/thumb.jpg"),
buildVariant(readyPublicAsset, "thumb", "WEBP", "asset/thumb.webp"),
buildVariant(readyPublicAsset, "hero", "AVIF", "asset/hero.avif"),
buildVariant(readyPublicAsset, "hero", "JPEG", "asset/hero.jpg")
));
List<PublicMediaUsageDto> result = service.getUsageMedia("home_section", "shop-gallery");
assertEquals(2, result.size());
assertEquals(1, result.get(0).getSortOrder());
assertEquals(Boolean.TRUE, result.get(0).getIsPrimary());
assertEquals("Shop hero", result.get(0).getTitle());
assertEquals("https://cdn.example/media/asset/thumb.jpg", result.get(0).getThumb().getJpegUrl());
assertEquals("https://cdn.example/media/asset/thumb.webp", result.get(0).getThumb().getWebpUrl());
assertEquals("https://cdn.example/media/asset/hero.avif", result.get(0).getHero().getAvifUrl());
}
@Test
void getUsageMedia_shouldReturnNullForMissingFormatsOrPresets() {
MediaAsset asset = buildAsset("READY", "PUBLIC", "Joe portrait", null);
MediaUsage usage = buildUsage(asset, "ABOUT_MEMBER", "joe", 0, true, true);
when(mediaUsageRepository.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
"ABOUT_MEMBER", "joe"
)).thenReturn(List.of(usage));
when(mediaVariantRepository.findByMediaAsset_IdIn(List.of(asset.getId())))
.thenReturn(List.of(buildVariant(asset, "card", "JPEG", "joe/card.jpg")));
List<PublicMediaUsageDto> result = service.getUsageMedia("ABOUT_MEMBER", "joe");
assertEquals(1, result.size());
assertNull(result.get(0).getThumb().getJpegUrl());
assertNull(result.get(0).getCard().getAvifUrl());
assertEquals("https://cdn.example/media/joe/card.jpg", result.get(0).getCard().getJpegUrl());
assertNull(result.get(0).getHero().getWebpUrl());
assertTrue(result.get(0).getIsPrimary());
}
private MediaAsset buildAsset(String status, String visibility, String title, String altText) {
MediaAsset asset = new MediaAsset();
asset.setId(UUID.randomUUID());
asset.setStatus(status);
asset.setVisibility(visibility);
asset.setTitle(title);
asset.setAltText(altText);
asset.setCreatedAt(OffsetDateTime.now());
asset.setUpdatedAt(OffsetDateTime.now());
return asset;
}
private MediaUsage buildUsage(MediaAsset asset,
String usageType,
String usageKey,
int sortOrder,
boolean isPrimary,
boolean isActive) {
MediaUsage usage = new MediaUsage();
usage.setId(UUID.randomUUID());
usage.setMediaAsset(asset);
usage.setUsageType(usageType);
usage.setUsageKey(usageKey);
usage.setSortOrder(sortOrder);
usage.setIsPrimary(isPrimary);
usage.setIsActive(isActive);
usage.setCreatedAt(OffsetDateTime.now().plusSeconds(sortOrder));
return usage;
}
private MediaVariant buildVariant(MediaAsset asset, String variantName, String format, String storageKey) {
MediaVariant variant = new MediaVariant();
variant.setId(UUID.randomUUID());
variant.setMediaAsset(asset);
variant.setVariantName(variantName);
variant.setFormat(format);
variant.setStorageKey(storageKey);
variant.setCreatedAt(OffsetDateTime.now());
return variant;
}
}