From 4b5cb6023c1cc5101ccdea59d1afc77705d787a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Thu, 23 Apr 2026 11:14:02 +0200 Subject: [PATCH] refactor+test: extend StringNormalizer, migrate SystemAdministratorService, add SystemAdministratorServiceTest - Add StringNormalizer.normalize() (trim + toLowerCase ROOT) for username normalization - Migrate SystemAdministratorService private normalize() to use StringNormalizer.normalize() - Remove now-unused Locale import from SystemAdministratorService - Add StringNormalizerTest coverage for new normalize() method - Add SystemAdministratorServiceTest with 10 unit tests covering all public methods --- .../service/SystemAdministratorService.java | 4 +- .../imgfloat/util/StringNormalizer.java | 9 ++ .../SystemAdministratorServiceTest.java | 104 ++++++++++++++++++ .../imgfloat/util/StringNormalizerTest.java | 11 ++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/test/java/dev/kruhlmann/imgfloat/service/SystemAdministratorServiceTest.java diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java b/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java index 53e2540..864fe9c 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/SystemAdministratorService.java @@ -2,8 +2,8 @@ package dev.kruhlmann.imgfloat.service; import dev.kruhlmann.imgfloat.model.db.imgfloat.SystemAdministrator; import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository; +import dev.kruhlmann.imgfloat.util.StringNormalizer; import java.util.List; -import java.util.Locale; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -137,6 +137,6 @@ public class SystemAdministratorService { } private String normalize(String username) { - return username.trim().toLowerCase(Locale.ROOT); + return StringNormalizer.normalize(username); } } diff --git a/src/main/java/dev/kruhlmann/imgfloat/util/StringNormalizer.java b/src/main/java/dev/kruhlmann/imgfloat/util/StringNormalizer.java index 3e3434b..e4f28f4 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/util/StringNormalizer.java +++ b/src/main/java/dev/kruhlmann/imgfloat/util/StringNormalizer.java @@ -16,4 +16,13 @@ public final class StringNormalizer { public static String toLowerCaseRoot(String value) { return value == null ? null : value.toLowerCase(Locale.ROOT); } + + /** + * Returns {@code value.trim().toLowerCase(Locale.ROOT)}. + * Useful for normalizing user-supplied identifiers such as Twitch usernames. + * Returns {@code null} if {@code value} is {@code null}. + */ + public static String normalize(String value) { + return value == null ? null : value.trim().toLowerCase(Locale.ROOT); + } } diff --git a/src/test/java/dev/kruhlmann/imgfloat/service/SystemAdministratorServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/service/SystemAdministratorServiceTest.java new file mode 100644 index 0000000..a29f8aa --- /dev/null +++ b/src/test/java/dev/kruhlmann/imgfloat/service/SystemAdministratorServiceTest.java @@ -0,0 +1,104 @@ +package dev.kruhlmann.imgfloat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +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 dev.kruhlmann.imgfloat.model.db.imgfloat.SystemAdministrator; +import dev.kruhlmann.imgfloat.repository.SystemAdministratorRepository; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.Environment; + +class SystemAdministratorServiceTest { + + private SystemAdministratorRepository repo; + private Environment environment; + private SystemAdministratorService service; + + @BeforeEach + void setup() { + repo = mock(SystemAdministratorRepository.class); + environment = mock(Environment.class); + when(environment.getProperty("IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN")).thenReturn("admin"); + when(environment.getProperty("org.springframework.boot.test.context.SpringBootTestContextBootstrapper")) + .thenReturn(null); + service = new SystemAdministratorService(repo, environment); + } + + @Test + void isSysadminReturnsTrueForInitialSysadmin() { + assertThat(service.isSysadmin("admin")).isTrue(); + assertThat(service.isSysadmin("ADMIN")).isTrue(); // case-insensitive + } + + @Test + void isSysadminDelegatesToRepositoryForOtherUsers() { + when(repo.existsByTwitchUsername("other")).thenReturn(true); + assertThat(service.isSysadmin("other")).isTrue(); + when(repo.existsByTwitchUsername("unknown")).thenReturn(false); + assertThat(service.isSysadmin("unknown")).isFalse(); + } + + @Test + void addSysadminSkipsIfAlreadyExists() { + when(repo.existsByTwitchUsername("newuser")).thenReturn(true); + service.addSysadmin("newuser"); + verify(repo, never()).save(any()); + } + + @Test + void addSysadminSavesNewUser() { + when(repo.existsByTwitchUsername("newuser")).thenReturn(false); + service.addSysadmin("newuser"); + verify(repo).save(any(SystemAdministrator.class)); + } + + @Test + void addSysadminThrowsForInitialSysadmin() { + assertThatThrownBy(() -> service.addSysadmin("admin")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("initial"); + } + + @Test + void removeSysadminThrowsForInitialSysadmin() { + assertThatThrownBy(() -> service.removeSysadmin("admin")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("initial"); + } + + @Test + void removeSysadminThrowsWhenUserDoesNotExist() { + when(repo.deleteByTwitchUsername("nonexistent")).thenReturn(0L); + assertThatThrownBy(() -> service.removeSysadmin("nonexistent")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void removeSysadminDeletesExistingUser() { + when(repo.deleteByTwitchUsername("user")).thenReturn(1L); + service.removeSysadmin("user"); + verify(repo).deleteByTwitchUsername("user"); + } + + @Test + void getInitialSysadminNormalizesUsername() { + when(environment.getProperty("IMGFLOAT_INITIAL_TWITCH_USERNAME_SYSADMIN")).thenReturn(" AdminUser "); + assertThat(service.getInitialSysadmin()).isEqualTo("adminuser"); + } + + @Test + void listSysadminsIncludesInitialSysadminFromEnvironment() { + SystemAdministrator persisted = new SystemAdministrator("other"); + when(repo.findAllByOrderByTwitchUsernameAsc()).thenReturn(List.of(persisted)); + List admins = service.listSysadmins(); + assertThat(admins).contains("admin", "other"); + } +} diff --git a/src/test/java/dev/kruhlmann/imgfloat/util/StringNormalizerTest.java b/src/test/java/dev/kruhlmann/imgfloat/util/StringNormalizerTest.java index cd7cb9b..fb3b645 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/util/StringNormalizerTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/util/StringNormalizerTest.java @@ -33,4 +33,15 @@ class StringNormalizerTest { // Turkish locale would uppercase 'i' to 'İ' but ROOT locale must not assertThat(StringNormalizer.toLowerCaseRoot("TITLE")).isEqualTo("title"); } + + @Test + void normalizeTrimsAndLowercases() { + assertThat(StringNormalizer.normalize(" Hello ")).isEqualTo("hello"); + assertThat(StringNormalizer.normalize("USER")).isEqualTo("user"); + } + + @Test + void normalizeReturnsNullForNull() { + assertThat(StringNormalizer.normalize(null)).isNull(); + } }