refactor+test: add MarketplaceService and SettingsService unit tests; use StringNormalizer in AccountService

This commit is contained in:
2026-04-21 16:15:25 +02:00
parent b6ca67d96c
commit 1a2e2344da
3 changed files with 214 additions and 2 deletions
@@ -7,8 +7,8 @@ import dev.kruhlmann.imgfloat.repository.ChannelRepository;
import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository; import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository;
import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository; import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository;
import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository; import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository;
import dev.kruhlmann.imgfloat.util.StringNormalizer;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -96,6 +96,6 @@ public class AccountService {
} }
private String normalize(String value) { private String normalize(String value) {
return value == null ? null : value.toLowerCase(Locale.ROOT); return StringNormalizer.toLowerCaseRoot(value);
} }
} }
@@ -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<ScriptMarketplaceEntry> entries = service.listScripts(null, null);
assertThat(entries).anyMatch(e -> "test-script".equals(e.id()));
}
@Test
void listScriptsFiltersEntriesByQuery() {
List<ScriptMarketplaceEntry> entries = service.listScripts("test", null);
assertThat(entries).anyMatch(e -> "test-script".equals(e.id()));
List<ScriptMarketplaceEntry> 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<ScriptMarketplaceEntry> 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<ScriptMarketplaceEntry> 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<ScriptMarketplaceEntry> withNull = service.listScripts(null, null);
List<ScriptMarketplaceEntry> withBlank = service.listScripts(" ", null);
assertThat(withBlank).hasSize(withNull.size());
}
}
@@ -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);
}
}