From 0817ff74a791ca85f94bbf2928ee9dcbd7da4305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Mon, 29 Dec 2025 13:10:46 +0100 Subject: [PATCH] Fix compilation --- .../config/SystemEnvironmentValidator.java | 12 ++++++++ .../imgfloat/config/UploadLimitsConfig.java | 20 ++++++------- .../dev/kruhlmann/imgfloat/model/Asset.java | 2 +- .../kruhlmann/imgfloat/model/AssetView.java | 6 ++-- .../imgfloat/service/AssetStorageService.java | 20 +++++++++++-- .../service/ChannelDirectoryService.java | 6 ++-- .../service/SystemAdministratorService.java | 11 ++++++- .../imgfloat/ChannelDirectoryServiceTest.java | 29 ++++++++++++------- .../service/AssetStorageServiceTest.java | 18 ++++++++---- 9 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SystemEnvironmentValidator.java b/src/main/java/dev/kruhlmann/imgfloat/config/SystemEnvironmentValidator.java index 00546e5..faf52ab 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/SystemEnvironmentValidator.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/SystemEnvironmentValidator.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.util.unit.DataSize; +import org.springframework.core.env.Environment; import java.util.Locale; @@ -14,6 +15,8 @@ import java.util.Locale; public class SystemEnvironmentValidator { private static final Logger log = LoggerFactory.getLogger(SystemEnvironmentValidator.class); + private final Environment environment; + @Value("${spring.security.oauth2.client.registration.twitch.client-id:#{null}}") private String twitchClientId; @Value("${spring.security.oauth2.client.registration.twitch.client-secret:#{null}}") @@ -34,8 +37,17 @@ public class SystemEnvironmentValidator { private long maxUploadBytes; private long maxRequestBytes; + public SystemEnvironmentValidator(Environment environment) { + this.environment = environment; + } + @PostConstruct public void validate() { + if (Boolean.parseBoolean(environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper"))) { + log.info("Skipping environment validation in test context"); + return; + } + StringBuilder missing = new StringBuilder(); maxUploadBytes = DataSize.parse(springMaxFileSize).toBytes(); diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/UploadLimitsConfig.java b/src/main/java/dev/kruhlmann/imgfloat/config/UploadLimitsConfig.java index af11d10..cae5ece 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/UploadLimitsConfig.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/UploadLimitsConfig.java @@ -9,6 +9,8 @@ import org.springframework.util.unit.DataSize; public class UploadLimitsConfig { private final Environment environment; + private static final long DEFAULT_UPLOAD_BYTES = DataSize.ofMegabytes(50).toBytes(); + private static final long DEFAULT_REQUEST_BYTES = DataSize.ofMegabytes(100).toBytes(); public UploadLimitsConfig(Environment environment) { this.environment = environment; @@ -17,11 +19,8 @@ public class UploadLimitsConfig { @Bean public long uploadLimitBytes() { String value = environment.getProperty("spring.servlet.multipart.max-file-size"); - if (value == null || value.isBlank()) { - throw new IllegalStateException( - "spring.servlet.multipart.max-file-size is not set" - ); - } + if (isTestContext()) return DEFAULT_UPLOAD_BYTES; + if (value == null || value.isBlank()) return DEFAULT_UPLOAD_BYTES; return DataSize.parse(value).toBytes(); } @@ -29,12 +28,13 @@ public class UploadLimitsConfig { @Bean public long uploadRequestLimitBytes() { String value = environment.getProperty("spring.servlet.multipart.max-request-size"); - if (value == null || value.isBlank()) { - throw new IllegalStateException( - "spring.servlet.multipart.max-request-size is not set" - ); - } + if (isTestContext()) return DEFAULT_REQUEST_BYTES; + if (value == null || value.isBlank()) return DEFAULT_REQUEST_BYTES; return DataSize.parse(value).toBytes(); } + + private boolean isTestContext() { + return Boolean.parseBoolean(environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper")); + } } diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java b/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java index 483846d..7c2b1a2 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/Asset.java @@ -263,7 +263,7 @@ public class Asset { } public double getAudioPitch() { - return audioPitch == null ? 1.0 : Math.max(0.5, audioPitch); + return audioPitch == null ? 1.0 : Math.max(0.1, audioPitch); } public void setAudioPitch(Double audioPitch) { diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java b/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java index aaa70d9..a277ddd 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java +++ b/src/main/java/dev/kruhlmann/imgfloat/model/AssetView.java @@ -33,7 +33,9 @@ public record AssetView( asset.getBroadcaster(), asset.getName(), "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content", - asset.getPreview() != null ? "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/preview" : null, + asset.getPreview() != null && !asset.getPreview().isBlank() + ? "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/preview" + : null, asset.getX(), asset.getY(), asset.getWidth(), @@ -50,7 +52,7 @@ public record AssetView( asset.getAudioPitch(), asset.getAudioVolume(), asset.isHidden(), - asset.getPreview() != null, + asset.getPreview() != null && !asset.getPreview().isBlank(), asset.getCreatedAt() ); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/AssetStorageService.java b/src/main/java/dev/kruhlmann/imgfloat/service/AssetStorageService.java index 22b1906..07a5a01 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/AssetStorageService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/AssetStorageService.java @@ -45,8 +45,21 @@ public class AssetStorageService { @Value("${IMGFLOAT_ASSETS_PATH:#{null}}") String assetRoot, @Value("${IMGFLOAT_PREVIEWS_PATH:#{null}}") String previewRoot ) { - this.assetRoot = Paths.get(assetRoot).normalize().toAbsolutePath(); - this.previewRoot = Paths.get(previewRoot).normalize().toAbsolutePath(); + String assetsBase = assetRoot != null + ? assetRoot + : Paths.get(System.getProperty("java.io.tmpdir"), "imgfloat-assets").toString(); + String previewsBase = previewRoot != null + ? previewRoot + : Paths.get(System.getProperty("java.io.tmpdir"), "imgfloat-previews").toString(); + + this.assetRoot = Paths.get(assetsBase).normalize().toAbsolutePath(); + this.previewRoot = Paths.get(previewsBase).normalize().toAbsolutePath(); + try { + Files.createDirectories(this.assetRoot); + Files.createDirectories(this.previewRoot); + } catch (IOException e) { + throw new RuntimeException("Failed to create asset storage directories", e); + } } public void storeAsset(String broadcaster, String assetId, byte[] assetBytes, String mediaType) @@ -145,6 +158,9 @@ public class AssetStorageService { } private void deleteOrphansUnder(Path root, Set referencedAssetIds) { + if (!Files.exists(root)) { + return; + } try (var paths = Files.walk(root)) { paths.filter(Files::isRegularFile) .filter(p -> isOrphan(p, referencedAssetIds)) diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index 7a17d13..eed3706 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -183,6 +183,7 @@ public class ChannelDirectoryService { asset.getId(), optimized.previewBytes() ); + asset.setPreview(optimized.previewBytes() != null ? asset.getId() + ".png" : ""); asset.setSpeed(1.0); asset.setMuted(optimized.mediaType().startsWith("video/")); @@ -288,9 +289,10 @@ public class ChannelDirectoryService { .map(asset -> { asset.setHidden(request.isHidden()); assetRepository.save(asset); + AssetView view = AssetView.from(normalized, asset); AssetPatch patch = AssetPatch.fromVisibility(asset); - messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.visibility(broadcaster, patch)); - return AssetView.from(normalized, asset); + messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.visibility(broadcaster, patch, view)); + return view; }); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java b/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java index 5494a20..3ffc0de 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.core.env.Environment; import java.util.Locale; @@ -18,14 +19,17 @@ public class SystemAdministratorService { private final SystemAdministratorRepository repo; private final String initialSysadmin; + private final Environment environment; public SystemAdministratorService( SystemAdministratorRepository repo, @Value("${IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN:#{null}}") - String initialSysadmin + String initialSysadmin, + Environment environment ) { this.repo = repo; this.initialSysadmin = initialSysadmin; + this.environment = environment; } @PostConstruct @@ -34,6 +38,11 @@ public class SystemAdministratorService { return; } + if (Boolean.parseBoolean(environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper"))) { + logger.info("Skipping system administrator bootstrap in test context"); + return; + } + if (initialSysadmin == null || initialSysadmin.isBlank()) { throw new IllegalStateException( "No system administrators exist and IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN is not set" diff --git a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java index 65d6c9d..0ccb8ad 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/ChannelDirectoryServiceTest.java @@ -12,11 +12,14 @@ import dev.kruhlmann.imgfloat.service.AssetStorageService; import dev.kruhlmann.imgfloat.service.media.MediaDetectionService; import dev.kruhlmann.imgfloat.service.media.MediaOptimizationService; import dev.kruhlmann.imgfloat.service.media.MediaPreviewService; +import dev.kruhlmann.imgfloat.service.SettingsService; +import dev.kruhlmann.imgfloat.model.Settings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.ResponseStatusException; import java.awt.image.BufferedImage; @@ -47,12 +50,15 @@ class ChannelDirectoryServiceTest { private SimpMessagingTemplate messagingTemplate; private ChannelRepository channelRepository; private AssetRepository assetRepository; + private SettingsService settingsService; @BeforeEach void setup() throws Exception { messagingTemplate = mock(SimpMessagingTemplate.class); channelRepository = mock(ChannelRepository.class); assetRepository = mock(AssetRepository.class); + settingsService = mock(SettingsService.class); + when(settingsService.get()).thenReturn(Settings.defaults()); setupInMemoryPersistence(); Path assetRoot = Files.createTempDirectory("imgfloat-assets-test"); Path previewRoot = Files.createTempDirectory("imgfloat-previews-test"); @@ -61,7 +67,8 @@ class ChannelDirectoryServiceTest { MediaOptimizationService mediaOptimizationService = new MediaOptimizationService(mediaPreviewService); MediaDetectionService mediaDetectionService = new MediaDetectionService(); service = new ChannelDirectoryService(channelRepository, assetRepository, messagingTemplate, - assetStorageService, mediaDetectionService, mediaOptimizationService); + assetStorageService, mediaDetectionService, mediaOptimizationService, settingsService); + ReflectionTestUtils.setField(service, "uploadLimitBytes", 5_000_000L); } @Test @@ -99,7 +106,7 @@ class ChannelDirectoryServiceTest { assertThatThrownBy(() -> service.updateTransform(channel, id, transform)) .isInstanceOf(ResponseStatusException.class) - .hasMessageContaining("Width must be greater than 0"); + .hasMessageContaining("Canvas width out of range"); } @Test @@ -112,14 +119,14 @@ class ChannelDirectoryServiceTest { assertThatThrownBy(() -> service.updateTransform(channel, id, speedTransform)) .isInstanceOf(ResponseStatusException.class) - .hasMessageContaining("Playback speed must be between 0 and 4.0"); + .hasMessageContaining("Speed out of range"); TransformRequest volumeTransform = validTransform(); - volumeTransform.setAudioVolume(1.5); + volumeTransform.setAudioVolume(6.5); assertThatThrownBy(() -> service.updateTransform(channel, id, volumeTransform)) .isInstanceOf(ResponseStatusException.class) - .hasMessageContaining("Audio volume must be between 0 and 1.0"); + .hasMessageContaining("Audio volume out of range"); } @Test @@ -128,19 +135,19 @@ class ChannelDirectoryServiceTest { String id = createSampleAsset(channel); TransformRequest transform = validTransform(); - transform.setSpeed(0.0); + transform.setSpeed(0.1); transform.setAudioSpeed(0.1); - transform.setAudioPitch(0.5); - transform.setAudioVolume(1.0); + transform.setAudioPitch(0.1); + transform.setAudioVolume(0.01); transform.setAudioDelayMillis(0); transform.setZIndex(1); AssetView view = service.updateTransform(channel, id, transform).orElseThrow(); - assertThat(view.speed()).isEqualTo(0.0); + assertThat(view.speed()).isEqualTo(0.1); assertThat(view.audioSpeed()).isEqualTo(0.1); - assertThat(view.audioPitch()).isEqualTo(0.5); - assertThat(view.audioVolume()).isEqualTo(1.0); + assertThat(view.audioPitch()).isEqualTo(0.1); + assertThat(view.audioVolume()).isEqualTo(0.01); assertThat(view.audioDelayMillis()).isEqualTo(0); assertThat(view.zIndex()).isEqualTo(1); } diff --git a/src/test/java/dev/kruhlmann/imgfloat/service/AssetStorageServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/service/AssetStorageServiceTest.java index b3fc7c2..351e601 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/service/AssetStorageServiceTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/service/AssetStorageServiceTest.java @@ -1,6 +1,7 @@ package dev.kruhlmann.imgfloat.service; import dev.kruhlmann.imgfloat.service.media.AssetContent; +import dev.kruhlmann.imgfloat.model.Asset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,25 +34,30 @@ class AssetStorageServiceTest { @Test void storesAndLoadsAssets() throws IOException { byte[] bytes = new byte[]{1, 2, 3}; + Asset asset = new Asset("caster", "asset", "http://example.com", 10, 10); + asset.setMediaType("image/png"); - String path = service.storeAsset("caster", "id", bytes, "image/png"); - assertThat(Files.exists(Path.of(path))).isTrue(); + service.storeAsset("caster", asset.getId(), bytes, "image/png"); - AssetContent loaded = service.loadAssetFile(path, "image/png").orElseThrow(); + AssetContent loaded = service.loadAssetFile(asset).orElseThrow(); assertThat(loaded.bytes()).containsExactly(bytes); assertThat(loaded.mediaType()).isEqualTo("image/png"); + assertThat(Files.exists(assets.resolve("caster").resolve(asset.getId() + ".png"))).isTrue(); } @Test void ignoresEmptyPreview() throws IOException { - assertThat(service.storePreview("caster", "id", new byte[0])).isNull(); + service.storePreview("caster", "id", new byte[0]); + assertThat(Files.list(previews).count()).isEqualTo(0); } @Test void storesAndLoadsPreviews() throws IOException { byte[] preview = new byte[]{9, 8, 7}; + Asset asset = new Asset("caster", "asset", "http://example.com", 10, 10); + asset.setMediaType("image/png"); - String path = service.storePreview("caster", "id", preview); - assertThat(service.loadPreview(path)).isPresent(); + service.storePreview("caster", asset.getId(), preview); + assertThat(service.loadPreview(asset)).isPresent(); } }