diff --git a/src/main/java/com/imgfloat/app/config/TaskConfig.java b/src/main/java/com/imgfloat/app/config/TaskConfig.java deleted file mode 100644 index e68b007..0000000 --- a/src/main/java/com/imgfloat/app/config/TaskConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.imgfloat.app.config; - -import org.springframework.boot.task.TaskExecutorBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; - -@Configuration -public class TaskConfig { - - @Bean(name = "assetTaskExecutor") - public TaskExecutor assetTaskExecutor(TaskExecutorBuilder builder) { - return builder - .threadNamePrefix("asset-optimizer-") - .build(); - } -} diff --git a/src/main/java/com/imgfloat/app/model/AssetEvent.java b/src/main/java/com/imgfloat/app/model/AssetEvent.java index e3219cd..71b1134 100644 --- a/src/main/java/com/imgfloat/app/model/AssetEvent.java +++ b/src/main/java/com/imgfloat/app/model/AssetEvent.java @@ -34,15 +34,6 @@ public class AssetEvent { return event; } - public static AssetEvent updated(String channel, AssetView asset) { - AssetEvent event = new AssetEvent(); - event.type = Type.UPDATED; - event.channel = channel; - event.payload = asset; - event.assetId = asset.id(); - return event; - } - public static AssetEvent play(String channel, AssetView asset, boolean play) { AssetEvent event = new AssetEvent(); event.type = Type.PLAY; diff --git a/src/main/java/com/imgfloat/app/model/Channel.java b/src/main/java/com/imgfloat/app/model/Channel.java index ed9fa4f..3d3f7c8 100644 --- a/src/main/java/com/imgfloat/app/model/Channel.java +++ b/src/main/java/com/imgfloat/app/model/Channel.java @@ -5,7 +5,6 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; -import jakarta.persistence.Index; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.PrePersist; @@ -19,17 +18,13 @@ import java.util.Set; import java.util.stream.Collectors; @Entity -@Table(name = "channels", indexes = { - @Index(name = "idx_channels_broadcaster", columnList = "broadcaster") -}) +@Table(name = "channels") public class Channel { @Id private String broadcaster; @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "channel_admins", - joinColumns = @JoinColumn(name = "channel_id"), - indexes = @Index(name = "idx_channel_admins_username", columnList = "admin_username")) + @CollectionTable(name = "channel_admins", joinColumns = @JoinColumn(name = "channel_id")) @Column(name = "admin_username") private Set admins = new HashSet<>(); diff --git a/src/main/java/com/imgfloat/app/repository/ChannelRepository.java b/src/main/java/com/imgfloat/app/repository/ChannelRepository.java index 6dc3a86..1e245c1 100644 --- a/src/main/java/com/imgfloat/app/repository/ChannelRepository.java +++ b/src/main/java/com/imgfloat/app/repository/ChannelRepository.java @@ -3,12 +3,5 @@ package com.imgfloat.app.repository; import com.imgfloat.app.model.Channel; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface ChannelRepository extends JpaRepository { - List findTop50ByBroadcasterContainingIgnoreCaseOrderByBroadcasterAsc(String broadcaster); - - List findTop50ByOrderByBroadcasterAsc(); - - List findByAdminsContaining(String username); } diff --git a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java index 29f425a..4c7b180 100644 --- a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java +++ b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java @@ -19,8 +19,6 @@ import org.jcodec.common.model.Picture; import org.jcodec.scale.AWTUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -30,8 +28,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.net.URI; import java.net.URLConnection; import java.nio.ByteBuffer; import java.nio.file.InvalidPathException; @@ -39,12 +35,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.nio.file.StandardCopyOption; import java.util.Base64; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Optional; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -62,21 +58,17 @@ public class ChannelDirectoryService { private static final int MIN_GIF_DELAY_MS = 20; private static final String PREVIEW_MEDIA_TYPE = "image/png"; private static final Path PREVIEW_ROOT = Paths.get("previews"); - private static final Path ASSET_ROOT = Paths.get("assets"); private static final Logger logger = LoggerFactory.getLogger(ChannelDirectoryService.class); private final ChannelRepository channelRepository; private final AssetRepository assetRepository; private final SimpMessagingTemplate messagingTemplate; - private final TaskExecutor assetTaskExecutor; public ChannelDirectoryService(ChannelRepository channelRepository, AssetRepository assetRepository, - SimpMessagingTemplate messagingTemplate, - @Qualifier("assetTaskExecutor") TaskExecutor assetTaskExecutor) { + SimpMessagingTemplate messagingTemplate) { this.channelRepository = channelRepository; this.assetRepository = assetRepository; this.messagingTemplate = messagingTemplate; - this.assetTaskExecutor = assetTaskExecutor; } public Channel getOrCreateChannel(String broadcaster) { @@ -87,13 +79,13 @@ public class ChannelDirectoryService { public List searchBroadcasters(String query) { String normalizedQuery = normalize(query); - if (normalizedQuery == null || normalizedQuery.isBlank()) { - return channelRepository.findTop50ByOrderByBroadcasterAsc().stream() - .map(Channel::getBroadcaster) - .toList(); - } - return channelRepository.findTop50ByBroadcasterContainingIgnoreCaseOrderByBroadcasterAsc(normalizedQuery).stream() + return channelRepository.findAll().stream() .map(Channel::getBroadcaster) + .map(this::normalize) + .filter(Objects::nonNull) + .filter(name -> normalizedQuery == null || normalizedQuery.isBlank() || name.contains(normalizedQuery)) + .sorted() + .limit(50) .toList(); } @@ -142,139 +134,39 @@ public class ChannelDirectoryService { public Optional createAsset(String broadcaster, MultipartFile file) throws IOException { Channel channel = getOrCreateChannel(broadcaster); - byte[] bytes; - try (InputStream stream = file.getInputStream()) { - bytes = stream.readAllBytes(); - } + byte[] bytes = file.getBytes(); String mediaType = detectMediaType(file, bytes); + OptimizedAsset optimized = optimizeAsset(bytes, mediaType); + if (optimized == null) { + return Optional.empty(); + } + String name = Optional.ofNullable(file.getOriginalFilename()) .map(filename -> filename.replaceAll("^.*[/\\\\]", "")) .filter(s -> !s.isBlank()) .orElse("Asset " + System.currentTimeMillis()); - Asset asset = buildPlaceholderAsset(channel, name, bytes, mediaType); - if (asset == null) { - return Optional.empty(); - } - if (!shouldOptimizeAsync(mediaType)) { - OptimizedAsset optimized = optimizeAsset(bytes, mediaType); - if (optimized == null) { - return Optional.empty(); - } - applyOptimizedAsset(channel.getBroadcaster(), asset, optimized); - } - assetRepository.save(asset); - AssetView view = AssetView.from(channel.getBroadcaster(), asset); - messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view)); - - if (shouldOptimizeAsync(mediaType)) { - enqueueOptimization(channel.getBroadcaster(), asset.getId(), bytes, mediaType); - } - - return Optional.of(view); - } - - private Asset buildPlaceholderAsset(Channel channel, String name, byte[] bytes, String mediaType) { - Dimension dimension = inferDimensions(bytes, mediaType); - double width = dimension.width() > 0 ? dimension.width() : (mediaType.startsWith("audio/") ? 400 : 640); - double height = dimension.height() > 0 ? dimension.height() : (mediaType.startsWith("audio/") ? 80 : 360); - Asset asset = new Asset(channel.getBroadcaster(), name, "", width, height); + String dataUrl = "data:" + optimized.mediaType() + ";base64," + Base64.getEncoder().encodeToString(optimized.bytes()); + double width = optimized.width() > 0 ? optimized.width() : (optimized.mediaType().startsWith("audio/") ? 400 : 640); + double height = optimized.height() > 0 ? optimized.height() : (optimized.mediaType().startsWith("audio/") ? 80 : 360); + Asset asset = new Asset(channel.getBroadcaster(), name, dataUrl, width, height); asset.setOriginalMediaType(mediaType); asset.setMediaType(optimized.mediaType()); - asset.setPreview(null); - asset.setUrl(storeAsset(channel.getBroadcaster(), asset.getId(), optimized.bytes(), optimized.mediaType())); + asset.setPreview(storePreview(channel.getBroadcaster(), asset.getId(), optimized.previewBytes())); asset.setSpeed(1.0); - asset.setMuted(mediaType.startsWith("video/")); + asset.setMuted(optimized.mediaType().startsWith("video/")); asset.setAudioLoop(false); asset.setAudioDelayMillis(0); asset.setAudioSpeed(1.0); asset.setAudioPitch(1.0); asset.setAudioVolume(1.0); asset.setZIndex(nextZIndex(channel.getBroadcaster())); - return asset; - } - private boolean shouldOptimizeAsync(String mediaType) { - return "image/gif".equalsIgnoreCase(mediaType) || mediaType.startsWith("video/"); - } - - private void enqueueOptimization(String broadcaster, String assetId, byte[] bytes, String mediaType) { - assetTaskExecutor.execute(() -> { - try { - OptimizedAsset optimized = optimizeAsset(bytes, mediaType); - if (optimized == null) { - return; - } - assetRepository.findById(assetId) - .filter(asset -> broadcaster.equals(asset.getBroadcaster())) - .ifPresent(asset -> { - boolean changed = applyOptimizedAsset(broadcaster, asset, optimized); - if (changed) { - assetRepository.save(asset); - AssetView view = AssetView.from(broadcaster, asset); - messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view)); - } - }); - } catch (Exception e) { - logger.warn("Unable to optimize asset {}", assetId, e); - } - }); - } - - private boolean applyOptimizedAsset(String broadcaster, Asset asset, OptimizedAsset optimized) { - if (optimized == null) { - return false; - } - boolean changed = false; - String optimizedUrl = toDataUrl(optimized.bytes(), optimized.mediaType()); - if (!optimizedUrl.equals(asset.getUrl())) { - asset.setUrl(optimizedUrl); - changed = true; - } - if (!Objects.equals(asset.getMediaType(), optimized.mediaType())) { - asset.setMediaType(optimized.mediaType()); - changed = true; - } - if (optimized.width() > 0 && optimized.height() > 0) { - asset.setWidth(optimized.width()); - asset.setHeight(optimized.height()); - changed = true; - } - asset.setMuted(optimized.mediaType().startsWith("video/")); - - String previousPreview = asset.getPreview(); - try { - asset.setPreview(storePreview(broadcaster, asset.getId(), optimized.previewBytes())); - if (!Objects.equals(previousPreview, asset.getPreview())) { - deletePreviewFile(previousPreview); - changed = changed || asset.getPreview() != null; - } - } catch (IOException e) { - logger.warn("Unable to store preview for asset {}", asset.getId(), e); - } - return changed; - } - - private Dimension inferDimensions(byte[] bytes, String mediaType) { - try { - if (mediaType.startsWith("video/")) { - return extractVideoDimensions(bytes); - } - if (mediaType.startsWith("image/")) { - BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes)); - if (image != null) { - return new Dimension(image.getWidth(), image.getHeight()); - } - } - } catch (IOException e) { - logger.warn("Unable to infer dimensions for {}", mediaType, e); - } - return new Dimension(0, 0); - } - - private String toDataUrl(byte[] bytes, String mediaType) { - return "data:" + mediaType + ";base64," + Base64.getEncoder().encodeToString(bytes); + assetRepository.save(asset); + AssetView view = AssetView.from(channel.getBroadcaster(), asset); + messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view)); + return Optional.of(view); } public Optional updateTransform(String broadcaster, String assetId, TransformRequest request) { @@ -352,7 +244,6 @@ public class ChannelDirectoryService { .filter(asset -> normalized.equals(asset.getBroadcaster())) .map(asset -> { deletePreviewFile(asset.getPreview()); - deleteAssetFile(asset.getUrl()); assetRepository.delete(asset); messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.deleted(broadcaster, assetId)); return true; @@ -410,7 +301,8 @@ public class ChannelDirectoryService { return List.of(); } String login = username.toLowerCase(); - return channelRepository.findByAdminsContaining(login).stream() + return channelRepository.findAll().stream() + .filter(channel -> channel.getAdmins().contains(login)) .map(Channel::getBroadcaster) .toList(); } @@ -432,18 +324,9 @@ public class ChannelDirectoryService { } private Optional decodeAssetData(Asset asset) { - if (asset.getUrl() == null) { - return Optional.empty(); - } - - if (asset.getUrl().startsWith("data:")) { - return decodeDataUrl(asset.getUrl()) - .flatMap(content -> backfillAssetStorage(asset, content)); - } - - return loadAssetFromStore(asset) + return decodeDataUrl(asset.getUrl()) .or(() -> { - logger.warn("Unable to read asset data for {}", asset.getId()); + logger.warn("Unable to decode asset data for {}", asset.getId()); return Optional.empty(); }); } @@ -490,88 +373,6 @@ public class ChannelDirectoryService { } } - private String storeAsset(String broadcaster, String assetId, byte[] bytes, String mediaType) throws IOException { - Path directory = ASSET_ROOT.resolve(normalize(broadcaster)); - Files.createDirectories(directory); - String extension = extensionFor(mediaType); - Path assetFile = directory.resolve(assetId + (extension == null ? "" : ("." + extension))); - try (InputStream in = new ByteArrayInputStream(bytes)) { - Files.copy(in, assetFile, StandardCopyOption.REPLACE_EXISTING); - } - return assetFile.toUri().toString(); - } - - private Optional loadAssetFromStore(Asset asset) { - String location = asset.getUrl(); - if (location == null || location.isBlank()) { - return Optional.empty(); - } - try { - URI uri = URI.create(location); - Path path; - if (uri.getScheme() == null) { - path = Paths.get(location); - } else if ("file".equalsIgnoreCase(uri.getScheme())) { - path = Paths.get(uri); - } else { - logger.warn("Unsupported asset URI scheme {} for {}", uri.getScheme(), asset.getId()); - return Optional.empty(); - } - if (path == null || !Files.exists(path)) { - logger.warn("Asset location {} is not readable", location); - return Optional.empty(); - } - try (InputStream stream = Files.newInputStream(path)) { - byte[] bytes = stream.readAllBytes(); - String mediaType = asset.getMediaType() == null ? "application/octet-stream" : asset.getMediaType(); - return Optional.of(new AssetContent(bytes, mediaType)); - } - } catch (IOException | IllegalArgumentException e) { - logger.warn("Unable to read asset data from {}", location, e); - return Optional.empty(); - } - } - - private Optional backfillAssetStorage(Asset asset, AssetContent content) { - try { - String storedLocation = storeAsset(asset.getBroadcaster(), asset.getId(), content.bytes(), - asset.getMediaType() == null ? content.mediaType() : asset.getMediaType()); - asset.setUrl(storedLocation); - assetRepository.save(asset); - return Optional.of(new AssetContent(content.bytes(), - asset.getMediaType() == null ? content.mediaType() : asset.getMediaType())); - } catch (IOException e) { - logger.warn("Unable to backfill storage for {}", asset.getId(), e); - return Optional.empty(); - } - } - - private void deleteAssetFile(String location) { - if (location == null || location.isBlank()) { - return; - } - try { - URI uri = URI.create(location); - Path path; - if (uri.getScheme() == null) { - path = Paths.get(location); - } else if ("file".equalsIgnoreCase(uri.getScheme())) { - path = Paths.get(uri); - } else { - return; - } - if (path != null) { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - logger.warn("Unable to delete asset file {}", path, e); - } - } - } catch (IllegalArgumentException e) { - logger.debug("Asset location {} is not a file path; skipping delete", location); - } - } - private String storePreview(String broadcaster, String assetId, byte[] previewBytes) throws IOException { if (previewBytes == null || previewBytes.length == 0) { return null; @@ -638,24 +439,6 @@ public class ChannelDirectoryService { .orElse("application/octet-stream"); } - private String extensionFor(String mediaType) { - if (mediaType == null) { - return null; - } - return switch (mediaType.toLowerCase(Locale.ROOT)) { - case "image/png" -> "png"; - case "image/jpeg" -> "jpg"; - case "image/gif" -> "gif"; - case "video/mp4" -> "mp4"; - case "video/webm" -> "webm"; - case "video/quicktime" -> "mov"; - case "audio/mpeg" -> "mp3"; - case "audio/wav" -> "wav"; - case "audio/ogg" -> "ogg"; - default -> null; - }; - } - private OptimizedAsset optimizeAsset(byte[] bytes, String mediaType) throws IOException { if ("image/gif".equalsIgnoreCase(mediaType)) { OptimizedAsset transcoded = transcodeGifToVideo(bytes); diff --git a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java index 233ca12..3d83755 100644 --- a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java +++ b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java @@ -11,24 +11,19 @@ import com.imgfloat.app.service.ChannelDirectoryService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.mock.web.MockMultipartFile; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ConcurrentHashMap; import javax.imageio.ImageIO; -import org.jcodec.api.awt.AWTSequenceEncoder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -43,18 +38,14 @@ class ChannelDirectoryServiceTest { private SimpMessagingTemplate messagingTemplate; private ChannelRepository channelRepository; private AssetRepository assetRepository; - private RecordingTaskExecutor taskExecutor; - private Map channels; - private Map assets; @BeforeEach void setup() { messagingTemplate = mock(SimpMessagingTemplate.class); channelRepository = mock(ChannelRepository.class); assetRepository = mock(AssetRepository.class); - taskExecutor = new RecordingTaskExecutor(); setupInMemoryPersistence(); - service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate, taskExecutor); + service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate); } @Test @@ -67,41 +58,6 @@ class ChannelDirectoryServiceTest { verify(messagingTemplate).convertAndSend(org.mockito.ArgumentMatchers.contains("/topic/channel/caster"), captor.capture()); } - @Test - void asyncGifOptimizationSwitchesToVideo() throws Exception { - MockMultipartFile file = new MockMultipartFile("file", "image.gif", "image/gif", largeGif()); - - long start = System.currentTimeMillis(); - AssetView created = service.createAsset("caster", file).orElseThrow(); - long duration = System.currentTimeMillis() - start; - - assertThat(duration).isLessThan(750L); - Asset placeholder = assets.get(created.id()); - assertThat(placeholder.getMediaType()).isEqualTo("image/gif"); - assertThat(placeholder.getPreview()).isNull(); - - taskExecutor.runAll(); - - Asset optimized = assets.get(created.id()); - assertThat(optimized.getMediaType()).isEqualTo("video/mp4"); - assertThat(optimized.getPreview()).isNotBlank(); - } - - @Test - void videoPreviewGeneratedInBackground() throws Exception { - MockMultipartFile file = new MockMultipartFile("file", "clip.mp4", "video/mp4", sampleVideo()); - - AssetView created = service.createAsset("caster", file).orElseThrow(); - Asset placeholder = assets.get(created.id()); - assertThat(placeholder.getPreview()).isNull(); - - taskExecutor.runAll(); - - Asset optimized = assets.get(created.id()); - assertThat(optimized.getPreview()).isNotBlank(); - assertThat(optimized.getMediaType()).isEqualTo("video/mp4"); - } - @Test void updatesTransformAndVisibility() throws Exception { MockMultipartFile file = new MockMultipartFile("file", "image.png", "image/png", samplePng()); @@ -129,33 +85,9 @@ class ChannelDirectoryServiceTest { return out.toByteArray(); } - private byte[] largeGif() throws IOException { - BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - ImageIO.write(image, "gif", out); - return out.toByteArray(); - } - } - - private byte[] sampleVideo() throws IOException { - File temp = File.createTempFile("sample", ".mp4"); - temp.deleteOnExit(); - try { - AWTSequenceEncoder encoder = AWTSequenceEncoder.createSequenceEncoder(temp, 10); - for (int i = 0; i < 15; i++) { - BufferedImage frame = new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB); - encoder.encodeImage(frame); - } - encoder.finish(); - return java.nio.file.Files.readAllBytes(temp.toPath()); - } finally { - temp.delete(); - } - } - private void setupInMemoryPersistence() { - channels = new ConcurrentHashMap<>(); - assets = new ConcurrentHashMap<>(); + Map channels = new ConcurrentHashMap<>(); + Map assets = new ConcurrentHashMap<>(); when(channelRepository.findById(anyString())) .thenAnswer(invocation -> Optional.ofNullable(channels.get(invocation.getArgument(0)))); @@ -190,18 +122,4 @@ class ChannelDirectoryServiceTest { .filter(asset -> !onlyVisible || !asset.isHidden()) .toList(); } - - private static class RecordingTaskExecutor implements TaskExecutor { - private final List queued = new CopyOnWriteArrayList<>(); - - @Override - public void execute(Runnable task) { - queued.add(task); - } - - void runAll() { - new ArrayList<>(queued).forEach(Runnable::run); - queued.clear(); - } - } } diff --git a/src/test/java/com/imgfloat/app/service/ChannelDirectoryServiceIntegrationTest.java b/src/test/java/com/imgfloat/app/service/ChannelDirectoryServiceIntegrationTest.java deleted file mode 100644 index c17da08..0000000 --- a/src/test/java/com/imgfloat/app/service/ChannelDirectoryServiceIntegrationTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.imgfloat.app.service; - -import com.imgfloat.app.model.Channel; -import com.imgfloat.app.repository.ChannelRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -import com.imgfloat.app.repository.AssetRepository; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@ExtendWith(SpringExtension.class) -@DataJpaTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class ChannelDirectoryServiceIntegrationTest { - - @Autowired - private ChannelRepository channelRepository; - - private ChannelDirectoryService channelDirectoryService; - - @BeforeEach - void setUp() { - channelDirectoryService = new ChannelDirectoryService( - channelRepository, - Mockito.mock(AssetRepository.class), - Mockito.mock(SimpMessagingTemplate.class)); - } - - @Test - void searchBroadcastersFindsPartialMatchesCaseInsensitive() { - Channel alpha = new Channel("Alpha"); - Channel alphabeta = new Channel("AlphaBeta"); - Channel bravo = new Channel("Bravo"); - channelRepository.saveAll(List.of(alpha, alphabeta, bravo)); - - List results = channelDirectoryService.searchBroadcasters("lPh"); - - assertThat(results).containsExactly("alpha", "alphabeta"); - } - - @Test - void adminChannelsForFindsNormalizedAdminEntries() { - Channel alpha = new Channel("Alpha"); - alpha.addAdmin("ModOne"); - Channel bravo = new Channel("Bravo"); - bravo.addAdmin("modone"); - Channel charlie = new Channel("Charlie"); - charlie.addAdmin("other"); - - channelRepository.saveAll(List.of(alpha, bravo, charlie)); - - List adminChannels = channelDirectoryService.adminChannelsFor("MODONE").stream().sorted().toList(); - - assertThat(adminChannels).containsExactly("alpha", "bravo"); - } -}