feat(back-end): admin home edit image page
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.printcalculator.dto.PublicMediaUsageDto;
|
||||
import com.printcalculator.service.media.PublicMediaQueryService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/public/media")
|
||||
@Transactional(readOnly = true)
|
||||
public class PublicMediaController {
|
||||
|
||||
private final PublicMediaQueryService publicMediaQueryService;
|
||||
|
||||
public PublicMediaController(PublicMediaQueryService publicMediaQueryService) {
|
||||
this.publicMediaQueryService = publicMediaQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/usages")
|
||||
public ResponseEntity<List<PublicMediaUsageDto>> getUsageMedia(@RequestParam String usageType,
|
||||
@RequestParam String usageKey) {
|
||||
return ResponseEntity.ok(publicMediaQueryService.getUsageMedia(usageType, usageKey));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.printcalculator.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PublicMediaUsageDto {
|
||||
private UUID mediaAssetId;
|
||||
private String title;
|
||||
private String altText;
|
||||
private String usageType;
|
||||
private String usageKey;
|
||||
private Integer sortOrder;
|
||||
private Boolean isPrimary;
|
||||
private PublicMediaVariantDto thumb;
|
||||
private PublicMediaVariantDto card;
|
||||
private PublicMediaVariantDto hero;
|
||||
|
||||
public UUID getMediaAssetId() {
|
||||
return mediaAssetId;
|
||||
}
|
||||
|
||||
public void setMediaAssetId(UUID mediaAssetId) {
|
||||
this.mediaAssetId = mediaAssetId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAltText() {
|
||||
return altText;
|
||||
}
|
||||
|
||||
public void setAltText(String altText) {
|
||||
this.altText = altText;
|
||||
}
|
||||
|
||||
public String getUsageType() {
|
||||
return usageType;
|
||||
}
|
||||
|
||||
public void setUsageType(String usageType) {
|
||||
this.usageType = usageType;
|
||||
}
|
||||
|
||||
public String getUsageKey() {
|
||||
return usageKey;
|
||||
}
|
||||
|
||||
public void setUsageKey(String usageKey) {
|
||||
this.usageKey = usageKey;
|
||||
}
|
||||
|
||||
public Integer getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(Integer sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public Boolean getIsPrimary() {
|
||||
return isPrimary;
|
||||
}
|
||||
|
||||
public void setIsPrimary(Boolean primary) {
|
||||
isPrimary = primary;
|
||||
}
|
||||
|
||||
public PublicMediaVariantDto getThumb() {
|
||||
return thumb;
|
||||
}
|
||||
|
||||
public void setThumb(PublicMediaVariantDto thumb) {
|
||||
this.thumb = thumb;
|
||||
}
|
||||
|
||||
public PublicMediaVariantDto getCard() {
|
||||
return card;
|
||||
}
|
||||
|
||||
public void setCard(PublicMediaVariantDto card) {
|
||||
this.card = card;
|
||||
}
|
||||
|
||||
public PublicMediaVariantDto getHero() {
|
||||
return hero;
|
||||
}
|
||||
|
||||
public void setHero(PublicMediaVariantDto hero) {
|
||||
this.hero = hero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.printcalculator.dto;
|
||||
|
||||
public class PublicMediaVariantDto {
|
||||
private String avifUrl;
|
||||
private String webpUrl;
|
||||
private String jpegUrl;
|
||||
|
||||
public String getAvifUrl() {
|
||||
return avifUrl;
|
||||
}
|
||||
|
||||
public void setAvifUrl(String avifUrl) {
|
||||
this.avifUrl = avifUrl;
|
||||
}
|
||||
|
||||
public String getWebpUrl() {
|
||||
return webpUrl;
|
||||
}
|
||||
|
||||
public void setWebpUrl(String webpUrl) {
|
||||
this.webpUrl = webpUrl;
|
||||
}
|
||||
|
||||
public String getJpegUrl() {
|
||||
return jpegUrl;
|
||||
}
|
||||
|
||||
public void setJpegUrl(String jpegUrl) {
|
||||
this.jpegUrl = jpegUrl;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ public interface MediaUsageRepository extends JpaRepository<MediaUsage, UUID> {
|
||||
|
||||
List<MediaUsage> findByMediaAsset_IdIn(Collection<UUID> mediaAssetIds);
|
||||
|
||||
List<MediaUsage> findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(String usageType,
|
||||
String usageKey);
|
||||
|
||||
@Query("""
|
||||
select usage from MediaUsage usage
|
||||
where usage.usageType = :usageType
|
||||
|
||||
@@ -328,6 +328,7 @@ public class AdminMediaControllerService {
|
||||
String storageFolder = extractStorageFolder(asset.getStorageKey());
|
||||
|
||||
List<PendingGeneratedVariant> pendingVariants = new ArrayList<>();
|
||||
Set<String> skippedFormats = new LinkedHashSet<>();
|
||||
for (PresetDefinition preset : PRESETS) {
|
||||
VariantDimensions dimensions = computeVariantDimensions(
|
||||
asset.getWidthPx(),
|
||||
@@ -336,6 +337,10 @@ public class AdminMediaControllerService {
|
||||
);
|
||||
|
||||
for (String format : List.of(FORMAT_JPEG, FORMAT_WEBP, FORMAT_AVIF)) {
|
||||
if (!mediaFfmpegService.canEncode(format)) {
|
||||
skippedFormats.add(format);
|
||||
continue;
|
||||
}
|
||||
String extension = GENERATED_FORMAT_EXTENSIONS.get(format);
|
||||
Path outputFile = generatedDirectory.resolve(preset.name() + "." + extension);
|
||||
mediaFfmpegService.generateVariant(sourceFile, outputFile, dimensions.widthPx(), dimensions.heightPx(), format);
|
||||
@@ -356,6 +361,14 @@ public class AdminMediaControllerService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!skippedFormats.isEmpty()) {
|
||||
logger.warn(
|
||||
"Skipping media formats for asset {} because FFmpeg encoders are unavailable: {}",
|
||||
asset.getId(),
|
||||
String.join(", ", skippedFormats)
|
||||
);
|
||||
}
|
||||
|
||||
List<String> storedKeys = new ArrayList<>();
|
||||
try {
|
||||
for (PendingGeneratedVariant pendingVariant : pendingVariants) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.printcalculator.service.media;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -9,15 +11,31 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class MediaFfmpegService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MediaFfmpegService.class);
|
||||
|
||||
private static final Map<String, List<String>> ENCODER_CANDIDATES = Map.of(
|
||||
"JPEG", List.of("mjpeg"),
|
||||
"WEBP", List.of("libwebp", "webp"),
|
||||
"AVIF", List.of("libaom-av1", "librav1e", "libsvtav1")
|
||||
);
|
||||
|
||||
private final String ffmpegPath;
|
||||
private final Set<String> availableEncoders;
|
||||
|
||||
public MediaFfmpegService(@Value("${media.ffmpeg.path:ffmpeg}") String ffmpegPath) {
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.availableEncoders = Collections.unmodifiableSet(loadAvailableEncoders());
|
||||
}
|
||||
|
||||
public void generateVariant(Path source, Path target, int widthPx, int heightPx, String format) throws IOException {
|
||||
@@ -25,6 +43,11 @@ public class MediaFfmpegService {
|
||||
throw new IllegalArgumentException("Variant dimensions must be positive.");
|
||||
}
|
||||
|
||||
String encoder = resolveEncoder(format);
|
||||
if (encoder == null) {
|
||||
throw new IOException("FFmpeg encoder not available for media format " + format + ".");
|
||||
}
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(ffmpegPath);
|
||||
command.add("-y");
|
||||
@@ -42,19 +65,19 @@ public class MediaFfmpegService {
|
||||
switch (format) {
|
||||
case "JPEG" -> {
|
||||
command.add("-c:v");
|
||||
command.add("mjpeg");
|
||||
command.add(encoder);
|
||||
command.add("-q:v");
|
||||
command.add("2");
|
||||
}
|
||||
case "WEBP" -> {
|
||||
command.add("-c:v");
|
||||
command.add("libwebp");
|
||||
command.add(encoder);
|
||||
command.add("-quality");
|
||||
command.add("82");
|
||||
}
|
||||
case "AVIF" -> {
|
||||
command.add("-c:v");
|
||||
command.add("libaom-av1");
|
||||
command.add(encoder);
|
||||
command.add("-still-picture");
|
||||
command.add("1");
|
||||
command.add("-crf");
|
||||
@@ -86,6 +109,68 @@ public class MediaFfmpegService {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canEncode(String format) {
|
||||
return resolveEncoder(format) != null;
|
||||
}
|
||||
|
||||
private String resolveEncoder(String format) {
|
||||
if (format == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> candidates = ENCODER_CANDIDATES.get(format.trim().toUpperCase(Locale.ROOT));
|
||||
if (candidates == null) {
|
||||
return null;
|
||||
}
|
||||
return candidates.stream()
|
||||
.filter(availableEncoders::contains)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Set<String> loadAvailableEncoders() {
|
||||
List<String> command = List.of(ffmpegPath, "-hide_banner", "-encoders");
|
||||
try {
|
||||
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
|
||||
String output;
|
||||
try (InputStream processStream = process.getInputStream()) {
|
||||
output = new String(processStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
logger.warn("Unable to inspect FFmpeg encoders. Falling back to empty encoder list.");
|
||||
return Set.of();
|
||||
}
|
||||
return parseAvailableEncoders(output);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Unable to inspect FFmpeg encoders. Falling back to empty encoder list.", e);
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> parseAvailableEncoders(String output) {
|
||||
if (output == null || output.isBlank()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
Set<String> encoders = new LinkedHashSet<>();
|
||||
for (String line : output.split("\\R")) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isBlank() || trimmed.startsWith("--") || trimmed.startsWith("Encoders:")) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.length() < 7) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = trimmed.split("\\s+", 3);
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
encoders.add(parts[1]);
|
||||
}
|
||||
return encoders;
|
||||
}
|
||||
|
||||
private String truncate(String output) {
|
||||
if (output == null || output.isBlank()) {
|
||||
return "";
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.printcalculator.service.media;
|
||||
|
||||
import com.printcalculator.dto.PublicMediaUsageDto;
|
||||
import com.printcalculator.dto.PublicMediaVariantDto;
|
||||
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.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
public class PublicMediaQueryService {
|
||||
|
||||
private static final String STATUS_READY = "READY";
|
||||
private static final String VISIBILITY_PUBLIC = "PUBLIC";
|
||||
private static final String FORMAT_JPEG = "JPEG";
|
||||
private static final String FORMAT_WEBP = "WEBP";
|
||||
private static final String FORMAT_AVIF = "AVIF";
|
||||
|
||||
private final MediaUsageRepository mediaUsageRepository;
|
||||
private final MediaVariantRepository mediaVariantRepository;
|
||||
private final MediaStorageService mediaStorageService;
|
||||
|
||||
public PublicMediaQueryService(MediaUsageRepository mediaUsageRepository,
|
||||
MediaVariantRepository mediaVariantRepository,
|
||||
MediaStorageService mediaStorageService) {
|
||||
this.mediaUsageRepository = mediaUsageRepository;
|
||||
this.mediaVariantRepository = mediaVariantRepository;
|
||||
this.mediaStorageService = mediaStorageService;
|
||||
}
|
||||
|
||||
public List<PublicMediaUsageDto> getUsageMedia(String usageType, String usageKey) {
|
||||
String normalizedUsageType = normalizeUsageType(usageType);
|
||||
String normalizedUsageKey = normalizeUsageKey(usageKey);
|
||||
|
||||
List<MediaUsage> usages = mediaUsageRepository
|
||||
.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
|
||||
normalizedUsageType,
|
||||
normalizedUsageKey
|
||||
)
|
||||
.stream()
|
||||
.filter(this::isPublicReadyUsage)
|
||||
.sorted(Comparator
|
||||
.comparing(MediaUsage::getSortOrder, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(MediaUsage::getCreatedAt, Comparator.nullsLast(OffsetDateTime::compareTo)))
|
||||
.toList();
|
||||
|
||||
if (usages.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<UUID> assetIds = usages.stream()
|
||||
.map(MediaUsage::getMediaAsset)
|
||||
.filter(Objects::nonNull)
|
||||
.map(MediaAsset::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Map<UUID, List<MediaVariant>> variantsByAssetId = mediaVariantRepository.findByMediaAsset_IdIn(assetIds)
|
||||
.stream()
|
||||
.filter(variant -> !Objects.equals("ORIGINAL", variant.getFormat()))
|
||||
.collect(Collectors.groupingBy(variant -> variant.getMediaAsset().getId()));
|
||||
|
||||
return usages.stream()
|
||||
.map(usage -> toDto(
|
||||
usage,
|
||||
variantsByAssetId.getOrDefault(usage.getMediaAsset().getId(), List.of())
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean isPublicReadyUsage(MediaUsage usage) {
|
||||
MediaAsset asset = usage.getMediaAsset();
|
||||
return asset != null
|
||||
&& STATUS_READY.equals(asset.getStatus())
|
||||
&& VISIBILITY_PUBLIC.equals(asset.getVisibility());
|
||||
}
|
||||
|
||||
private PublicMediaUsageDto toDto(MediaUsage usage, List<MediaVariant> variants) {
|
||||
Map<String, Map<String, MediaVariant>> variantsByPresetAndFormat = variants.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
MediaVariant::getVariantName,
|
||||
Collectors.toMap(MediaVariant::getFormat, Function.identity(), (left, right) -> right)
|
||||
));
|
||||
|
||||
PublicMediaUsageDto dto = new PublicMediaUsageDto();
|
||||
dto.setMediaAssetId(usage.getMediaAsset().getId());
|
||||
dto.setTitle(usage.getMediaAsset().getTitle());
|
||||
dto.setAltText(usage.getMediaAsset().getAltText());
|
||||
dto.setUsageType(usage.getUsageType());
|
||||
dto.setUsageKey(usage.getUsageKey());
|
||||
dto.setSortOrder(usage.getSortOrder());
|
||||
dto.setIsPrimary(usage.getIsPrimary());
|
||||
dto.setThumb(buildPresetDto(variantsByPresetAndFormat.get("thumb")));
|
||||
dto.setCard(buildPresetDto(variantsByPresetAndFormat.get("card")));
|
||||
dto.setHero(buildPresetDto(variantsByPresetAndFormat.get("hero")));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PublicMediaVariantDto buildPresetDto(Map<String, MediaVariant> variantsByFormat) {
|
||||
PublicMediaVariantDto dto = new PublicMediaVariantDto();
|
||||
if (variantsByFormat == null || variantsByFormat.isEmpty()) {
|
||||
return dto;
|
||||
}
|
||||
|
||||
dto.setAvifUrl(buildVariantUrl(variantsByFormat.get(FORMAT_AVIF)));
|
||||
dto.setWebpUrl(buildVariantUrl(variantsByFormat.get(FORMAT_WEBP)));
|
||||
dto.setJpegUrl(buildVariantUrl(variantsByFormat.get(FORMAT_JPEG)));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String buildVariantUrl(MediaVariant variant) {
|
||||
if (variant == null || variant.getStorageKey() == null || variant.getStorageKey().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return mediaStorageService.buildPublicUrl(variant.getStorageKey());
|
||||
}
|
||||
|
||||
private String normalizeUsageType(String usageType) {
|
||||
if (usageType == null || usageType.isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "usageType is required.");
|
||||
}
|
||||
return usageType.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeUsageKey(String usageKey) {
|
||||
if (usageKey == null || usageKey.isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "usageKey is required.");
|
||||
}
|
||||
return usageKey.trim();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user