From 1a2e2344da42c88c042a2ff8fff94ee700aab2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Tue, 21 Apr 2026 16:15:25 +0200 Subject: [PATCH] refactor+test: add MarketplaceService and SettingsService unit tests; use StringNormalizer in AccountService --- .../imgfloat/service/AccountService.java | 4 +- .../service/MarketplaceServiceTest.java | 113 ++++++++++++++++++ .../imgfloat/service/SettingsServiceTest.java | 99 +++++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/test/java/dev/kruhlmann/imgfloat/service/MarketplaceServiceTest.java create mode 100644 src/test/java/dev/kruhlmann/imgfloat/service/SettingsServiceTest.java diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/AccountService.java b/src/main/java/dev/kruhlmann/imgfloat/service/AccountService.java index 6b92fd8..b884e31 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/AccountService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/AccountService.java @@ -7,8 +7,8 @@ import dev.kruhlmann.imgfloat.repository.ChannelRepository; import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository; import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository; +import dev.kruhlmann.imgfloat.util.StringNormalizer; import java.util.List; -import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; @@ -96,6 +96,6 @@ public class AccountService { } private String normalize(String value) { - return value == null ? null : value.toLowerCase(Locale.ROOT); + return StringNormalizer.toLowerCaseRoot(value); } } diff --git a/src/test/java/dev/kruhlmann/imgfloat/service/MarketplaceServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/service/MarketplaceServiceTest.java new file mode 100644 index 0000000..b3be6a7 --- /dev/null +++ b/src/test/java/dev/kruhlmann/imgfloat/service/MarketplaceServiceTest.java @@ -0,0 +1,113 @@ +package dev.kruhlmann.imgfloat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry; +import dev.kruhlmann.imgfloat.model.db.imgfloat.MarketplaceScriptHeart; +import dev.kruhlmann.imgfloat.model.db.imgfloat.ScriptAsset; +import dev.kruhlmann.imgfloat.repository.AssetRepository; +import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository; +import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository; +import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MarketplaceServiceTest { + + private MarketplaceService service; + private ScriptAssetRepository scriptAssetRepository; + private MarketplaceScriptHeartRepository heartRepository; + private MarketplaceScriptSeedLoader seedLoader; + + @BeforeEach + void setup() throws Exception { + scriptAssetRepository = mock(ScriptAssetRepository.class); + AssetRepository assetRepository = mock(AssetRepository.class); + ScriptAssetFileRepository scriptAssetFileRepository = mock(ScriptAssetFileRepository.class); + AssetStorageService assetStorageService = mock(AssetStorageService.class); + heartRepository = mock(MarketplaceScriptHeartRepository.class); + + when(scriptAssetRepository.findByIsPublicTrue()).thenReturn(List.of()); + when(heartRepository.countByScriptIds(anyList())).thenReturn(List.of()); + when(heartRepository.findByUsernameAndScriptIdIn(anyString(), anyList())).thenReturn(List.of()); + + Path marketplaceRoot = Files.createTempDirectory("imgfloat-marketplace-test"); + Path scriptDir = marketplaceRoot.resolve("test-script"); + Files.createDirectories(scriptDir); + Files.writeString(scriptDir.resolve("metadata.json"), + "{\"name\":\"Test Script\",\"description\":\"A test script\"}"); + Files.writeString(scriptDir.resolve("source.js"), "exports.init = function() {};"); + + seedLoader = new MarketplaceScriptSeedLoader(marketplaceRoot.toString()); + service = new MarketplaceService( + seedLoader, + scriptAssetRepository, + assetRepository, + scriptAssetFileRepository, + assetStorageService, + heartRepository + ); + } + + @Test + void listScriptsReturnsSeedEntries() { + List entries = service.listScripts(null, null); + assertThat(entries).anyMatch(e -> "test-script".equals(e.id())); + } + + @Test + void listScriptsFiltersEntriesByQuery() { + List entries = service.listScripts("test", null); + assertThat(entries).anyMatch(e -> "test-script".equals(e.id())); + + List noMatch = service.listScripts("zzznomatch", null); + assertThat(noMatch).noneMatch(e -> "test-script".equals(e.id())); + } + + @Test + void listScriptsIncludesPublicDatabaseScripts() { + ScriptAsset publicScript = new ScriptAsset(); + publicScript.setId("db-script"); + publicScript.setName("DB Script"); + publicScript.setPublic(true); + when(scriptAssetRepository.findByIsPublicTrue()).thenReturn(List.of(publicScript)); + when(heartRepository.countByScriptIds(anyList())).thenReturn(List.of()); + + List entries = service.listScripts(null, null); + assertThat(entries).anyMatch(e -> "db-script".equals(e.id())); + } + + @Test + void toggleHeartAddsHeartWhenNotAlreadyHearted() { + String scriptId = "test-script"; + String username = "user1"; + when(heartRepository.existsByScriptIdAndUsername(scriptId, username)).thenReturn(false); + when(heartRepository.countByScriptId(scriptId)).thenReturn(1L); + + Optional result = service.toggleHeart(scriptId, username); + assertThat(result).isPresent(); + } + + @Test + void toggleHeartReturnsEmptyForBlankInputs() { + assertThat(service.toggleHeart(null, "user")).isEmpty(); + assertThat(service.toggleHeart("script", null)).isEmpty(); + assertThat(service.toggleHeart("", "user")).isEmpty(); + } + + @Test + void listScriptsWithBlankQueryTreatsAsNoFilter() { + List withNull = service.listScripts(null, null); + List withBlank = service.listScripts(" ", null); + assertThat(withBlank).hasSize(withNull.size()); + } +} diff --git a/src/test/java/dev/kruhlmann/imgfloat/service/SettingsServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/service/SettingsServiceTest.java new file mode 100644 index 0000000..299422a --- /dev/null +++ b/src/test/java/dev/kruhlmann/imgfloat/service/SettingsServiceTest.java @@ -0,0 +1,99 @@ +package dev.kruhlmann.imgfloat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.kruhlmann.imgfloat.model.db.imgfloat.AudioAsset; +import dev.kruhlmann.imgfloat.model.db.imgfloat.Settings; +import dev.kruhlmann.imgfloat.model.db.imgfloat.VisualAsset; +import dev.kruhlmann.imgfloat.repository.AudioAssetRepository; +import dev.kruhlmann.imgfloat.repository.SettingsRepository; +import dev.kruhlmann.imgfloat.repository.VisualAssetRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SettingsServiceTest { + + private SettingsRepository repo; + private VisualAssetRepository visualAssetRepository; + private AudioAssetRepository audioAssetRepository; + private SettingsService service; + + @BeforeEach + void setup() { + repo = mock(SettingsRepository.class); + visualAssetRepository = mock(VisualAssetRepository.class); + audioAssetRepository = mock(AudioAssetRepository.class); + when(visualAssetRepository.findAll()).thenReturn(List.of()); + when(audioAssetRepository.findAll()).thenReturn(List.of()); + service = new SettingsService(repo, visualAssetRepository, audioAssetRepository, new ObjectMapper()); + } + + @Test + void initDefaultsCreatesSettingsWhenAbsent() { + when(repo.existsById(1)).thenReturn(false); + service.initDefaults(); + verify(repo).save(any(Settings.class)); + } + + @Test + void initDefaultsSkipsWhenAlreadyPresent() { + when(repo.existsById(1)).thenReturn(true); + service.initDefaults(); + verify(repo, never()).save(any()); + } + + @Test + void getReturnsStoredSettings() { + Settings stored = Settings.defaults(); + stored.setId(1); + when(repo.findById(1)).thenReturn(Optional.of(stored)); + assertThat(service.get()).isSameAs(stored); + } + + @Test + void saveClampsVisualAssetSpeedToNewRange() { + VisualAsset visual = new VisualAsset(); + visual.setSpeed(5.0); // exceeds max + visual.setAudioVolume(0.5); + when(visualAssetRepository.findAll()).thenReturn(List.of(visual)); + + Settings settings = Settings.defaults(); + settings.setMaxAssetPlaybackSpeedFraction(2.0); + settings.setMinAssetPlaybackSpeedFraction(0.1); + settings.setId(1); + when(repo.save(any())).thenReturn(settings); + + service.save(settings); + + verify(visualAssetRepository).saveAll(any()); + assertThat(visual.getSpeed()).isEqualTo(2.0); + } + + @Test + void saveClampsAudioAssetPitchToNewRange() { + AudioAsset audio = new AudioAsset(); + audio.setAudioSpeed(1.0); + audio.setAudioPitch(3.0); // exceeds max + audio.setAudioVolume(0.5); + when(audioAssetRepository.findAll()).thenReturn(List.of(audio)); + + Settings settings = Settings.defaults(); + settings.setMaxAssetAudioPitchFraction(2.0); + settings.setMinAssetAudioPitchFraction(0.5); + settings.setId(1); + when(repo.save(any())).thenReturn(settings); + + service.save(settings); + + verify(audioAssetRepository).saveAll(any()); + assertThat(audio.getAudioPitch()).isEqualTo(2.0); + } +}