Add tests for VersionService, GithubReleaseService, AuditLogService, AuthorizationService, MarketplaceScriptSeedLoader; fix duplicate import in ChannelDirectoryService

This commit is contained in:
2026-04-21 15:23:40 +02:00
parent 3bcd6d6747
commit b741fb176a
6 changed files with 523 additions and 1 deletions
@@ -8,7 +8,6 @@ import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest; import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest; import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest; import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest; import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest; import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest; import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
@@ -0,0 +1,111 @@
package dev.kruhlmann.imgfloat.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import dev.kruhlmann.imgfloat.model.db.audit.AuditLogEntry;
import dev.kruhlmann.imgfloat.repository.audit.AuditLogRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.dao.DataAccessResourceFailureException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class AuditLogServiceTest {
private AuditLogRepository repository;
private AuditLogService service;
@BeforeEach
void setup() {
repository = mock(AuditLogRepository.class);
service = new AuditLogService(repository);
}
@Test
void recordEntryPersistesNormalizedEntry() {
service.recordEntry("BroadCaster", "Actor", "ACTION", "some details");
ArgumentCaptor<AuditLogEntry> captor = ArgumentCaptor.forClass(AuditLogEntry.class);
verify(repository).save(captor.capture());
AuditLogEntry entry = captor.getValue();
assertThat(entry.getBroadcaster()).isEqualTo("broadcaster");
assertThat(entry.getActor()).isEqualTo("actor");
assertThat(entry.getAction()).isEqualTo("ACTION");
assertThat(entry.getDetails()).isEqualTo("some details");
}
@Test
void recordEntrySkipsBlankBroadcaster() {
service.recordEntry(" ", "actor", "ACTION", "details");
verify(repository, never()).save(any());
}
@Test
void recordEntrySkipsNullBroadcaster() {
service.recordEntry(null, "actor", "ACTION", "details");
verify(repository, never()).save(any());
}
@Test
void recordEntryUsesDefaultActorWhenNull() {
service.recordEntry("broadcaster", null, "ACTION", "details");
ArgumentCaptor<AuditLogEntry> captor = ArgumentCaptor.forClass(AuditLogEntry.class);
verify(repository).save(captor.capture());
assertThat(captor.getValue().getActor()).isEqualTo("system");
}
@Test
void recordEntryUsesDefaultActorWhenBlank() {
service.recordEntry("broadcaster", " ", "ACTION", "details");
ArgumentCaptor<AuditLogEntry> captor = ArgumentCaptor.forClass(AuditLogEntry.class);
verify(repository).save(captor.capture());
assertThat(captor.getValue().getActor()).isEqualTo("system");
}
@Test
void recordEntryDoesNotThrowWhenRepositoryFails() {
doThrow(new DataAccessResourceFailureException("db down")).when(repository).save(any());
// Must not propagate
service.recordEntry("broadcaster", "actor", "ACTION", "details");
}
@Test
void listEntriesReturnsEmptyPageForBlankBroadcaster() {
Page<AuditLogEntry> result = service.listEntries(" ", null, null, null, 0, 20);
assertThat(result.isEmpty()).isTrue();
verify(repository, never()).searchEntries(any(), any(), any(), any(), any());
}
@Test
void listEntriesClampsSizeAndDelegatesToRepository() {
when(repository.searchEntries(any(), any(), any(), any(), any()))
.thenReturn(new PageImpl<>(List.of()));
service.listEntries("broadcaster", null, null, null, 0, 999);
ArgumentCaptor<Pageable> pageableCaptor = ArgumentCaptor.forClass(Pageable.class);
verify(repository).searchEntries(any(), any(), any(), any(), pageableCaptor.capture());
assertThat(pageableCaptor.getValue().getPageSize()).isEqualTo(200);
}
@Test
void deleteEntriesForBroadcasterDelegatesToRepository() {
service.deleteEntriesForBroadcaster("Broadcaster");
verify(repository).deleteByBroadcaster("broadcaster");
}
@Test
void deleteEntriesSkipsBlankBroadcaster() {
service.deleteEntriesForBroadcaster(" ");
verify(repository, never()).deleteByBroadcaster(any());
}
}
@@ -0,0 +1,149 @@
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.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.server.ResponseStatusException;
class AuthorizationServiceTest {
private ChannelDirectoryService channelDirectoryService;
private SystemAdministratorService sysadminService;
private AuthorizationService authorizationService;
private AuthorizationService authorizationServiceSysadminDisabled;
@BeforeEach
void setup() {
channelDirectoryService = mock(ChannelDirectoryService.class);
sysadminService = mock(SystemAdministratorService.class);
authorizationService = new AuthorizationService(channelDirectoryService, sysadminService, true);
authorizationServiceSysadminDisabled = new AuthorizationService(channelDirectoryService, sysadminService, false);
}
// --- userMatchesSessionUsernameOrThrowHttpError ---
@Test
void matchesSessionUsernamePassesWhenEqual() {
// must not throw
authorizationService.userMatchesSessionUsernameOrThrowHttpError("alice", "alice");
}
@Test
void matchesSessionUsernameThrowsWhenSessionNull() {
assertThatThrownBy(() -> authorizationService.userMatchesSessionUsernameOrThrowHttpError("alice", null))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("logged in");
}
@Test
void matchesSessionUsernameThrowsWhenSubmittedNull() {
assertThatThrownBy(() -> authorizationService.userMatchesSessionUsernameOrThrowHttpError(null, "alice"))
.isInstanceOf(ResponseStatusException.class);
}
@Test
void matchesSessionUsernameThrowsWhenDifferent() {
assertThatThrownBy(() -> authorizationService.userMatchesSessionUsernameOrThrowHttpError("alice", "bob"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("not this user");
}
// --- userIsBroadcaster ---
@Test
void userIsBroadcasterReturnsTrueWhenEqual() {
assertThat(authorizationService.userIsBroadcaster("alice", "alice")).isTrue();
}
@Test
void userIsBroadcasterReturnsFalseWhenDifferent() {
assertThat(authorizationService.userIsBroadcaster("alice", "bob")).isFalse();
}
@Test
void userIsBroadcasterReturnsFalseWhenEitherNull() {
assertThat(authorizationService.userIsBroadcaster(null, "alice")).isFalse();
assertThat(authorizationService.userIsBroadcaster("alice", null)).isFalse();
}
// --- userIsChannelAdminForBroadcaster ---
@Test
void channelAdminCheckDelegatesToChannelDirectoryService() {
when(channelDirectoryService.isAdmin("broadcaster", "alice")).thenReturn(true);
assertThat(authorizationService.userIsChannelAdminForBroadcaster("broadcaster", "alice")).isTrue();
}
@Test
void channelAdminCheckReturnsFalseWhenNull() {
assertThat(authorizationService.userIsChannelAdminForBroadcaster(null, "alice")).isFalse();
assertThat(authorizationService.userIsChannelAdminForBroadcaster("broadcaster", null)).isFalse();
}
// --- userIsSystemAdministrator ---
@Test
void sysadminCheckDelegatesToSysadminService() {
when(sysadminService.isSysadmin("admin")).thenReturn(true);
assertThat(authorizationService.userIsSystemAdministrator("admin")).isTrue();
}
@Test
void sysadminCheckReturnsFalseWhenNull() {
assertThat(authorizationService.userIsSystemAdministrator(null)).isFalse();
}
// --- userIsBroadcasterOrChannelAdmin ---
@Test
void allowsWhenBroadcasterMatchesSelf() {
assertThat(authorizationService.userIsBroadcasterOrChannelAdminForBroadcaster("alice", "alice")).isTrue();
}
@Test
void allowsChannelAdmin() {
when(channelDirectoryService.isAdmin("broadcaster", "admin")).thenReturn(true);
assertThat(authorizationService.userIsBroadcasterOrChannelAdminForBroadcaster("broadcaster", "admin")).isTrue();
}
@Test
void allowsSysadminWhenAccessEnabled() {
when(sysadminService.isSysadmin("sysadmin")).thenReturn(true);
assertThat(authorizationService.userIsBroadcasterOrChannelAdminForBroadcaster("broadcaster", "sysadmin")).isTrue();
}
@Test
void deniedSysadminWhenAccessDisabled() {
when(sysadminService.isSysadmin("sysadmin")).thenReturn(true);
when(channelDirectoryService.isAdmin("broadcaster", "sysadmin")).thenReturn(false);
assertThat(authorizationServiceSysadminDisabled.userIsBroadcasterOrChannelAdminForBroadcaster("broadcaster", "sysadmin")).isFalse();
}
@Test
void deniesRandomUser() {
when(channelDirectoryService.isAdmin("broadcaster", "random")).thenReturn(false);
when(sysadminService.isSysadmin("random")).thenReturn(false);
assertThat(authorizationService.userIsBroadcasterOrChannelAdminForBroadcaster("broadcaster", "random")).isFalse();
}
// --- throw-based wrappers ---
@Test
void throwsWhenNotBroadcasterOrAdmin() {
when(channelDirectoryService.isAdmin("broadcaster", "random")).thenReturn(false);
when(sysadminService.isSysadmin("random")).thenReturn(false);
assertThatThrownBy(
() -> authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError("broadcaster", "random")
).isInstanceOf(ResponseStatusException.class);
}
@Test
void throwsWhenNotSysadmin() {
when(sysadminService.isSysadmin("user")).thenReturn(false);
assertThatThrownBy(() -> authorizationService.userIsSystemAdministratorOrThrowHttpError("user"))
.isInstanceOf(ResponseStatusException.class);
}
}
@@ -0,0 +1,56 @@
package dev.kruhlmann.imgfloat.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.Test;
class GithubReleaseServiceTest {
@Test
void returnsDownloadBaseUrlWhenConfigured() {
GithubReleaseService service = new GithubReleaseService("imgfloat", "client", "1.2.3");
String url = service.getDownloadBaseUrl();
assertThat(url).startsWith("https://github.com/imgfloat/client/releases/download/v1.2.3/");
}
@Test
void stripsLeadingVFromVersion() {
GithubReleaseService service = new GithubReleaseService("imgfloat", "client", "v1.2.3");
assertThat(service.getDownloadBaseUrl()).contains("/v1.2.3/");
assertThat(service.getClientReleaseVersion()).isEqualTo("1.2.3");
}
@Test
void stripsSnapshotSuffix() {
GithubReleaseService service = new GithubReleaseService("imgfloat", "client", "1.2.3-SNAPSHOT");
assertThat(service.getClientReleaseVersion()).isEqualTo("1.2.3");
assertThat(service.getDownloadBaseUrl()).contains("/v1.2.3/");
}
@Test
void throwsWhenOwnerMissing() {
GithubReleaseService service = new GithubReleaseService(null, "client", "1.0.0");
assertThatThrownBy(service::getDownloadBaseUrl)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("GitHub client configuration");
}
@Test
void throwsWhenRepoMissing() {
GithubReleaseService service = new GithubReleaseService("imgfloat", null, "1.0.0");
assertThatThrownBy(service::getDownloadBaseUrl).isInstanceOf(IllegalStateException.class);
}
@Test
void throwsWhenVersionMissing() {
GithubReleaseService service = new GithubReleaseService("imgfloat", "client", null);
assertThatThrownBy(service::getDownloadBaseUrl).isInstanceOf(IllegalStateException.class);
}
@Test
void throwsWhenVersionBlank() {
GithubReleaseService service = new GithubReleaseService("imgfloat", "client", " ");
assertThatThrownBy(service::getDownloadBaseUrl).isInstanceOf(IllegalStateException.class);
}
}
@@ -0,0 +1,181 @@
package dev.kruhlmann.imgfloat.service;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class MarketplaceScriptSeedLoaderTest {
@TempDir
Path tempDir;
private void writeScript(Path dir, String name, String description, String source) throws IOException {
Files.createDirectories(dir);
String metaJson = description != null
? "{\"name\":\"" + name + "\",\"description\":\"" + description + "\"}"
: "{\"name\":\"" + name + "\"}";
Files.writeString(dir.resolve("metadata.json"), metaJson);
Files.writeString(dir.resolve("source.js"), source);
}
@Test
void returnsEmptyListWhenRootPathIsNonExistentDirectory() throws IOException {
// Pass a path that does not exist and is not the fallback doc/marketplace-scripts
Path nonExistent = tempDir.resolve("does-not-exist");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(nonExistent.toString());
assertThat(loader.listEntriesForQuery(null)).isEmpty();
}
@Test
void loadsScriptFromDirectory() throws IOException {
Path scriptDir = tempDir.resolve("my-script");
writeScript(scriptDir, "My Script", "A test script", "console.log('hi');");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
List<ScriptMarketplaceEntry> entries = loader.listEntriesForQuery(null);
assertThat(entries).hasSize(1);
assertThat(entries.get(0).id()).isEqualTo("my-script");
assertThat(entries.get(0).name()).isEqualTo("My Script");
assertThat(entries.get(0).description()).isEqualTo("A test script");
}
@Test
void skipsDirectoryWithMissingMetadata() throws IOException {
Path scriptDir = tempDir.resolve("no-meta");
Files.createDirectories(scriptDir);
Files.writeString(scriptDir.resolve("source.js"), "/* source */");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
assertThat(loader.listEntriesForQuery(null)).isEmpty();
}
@Test
void skipsDirectoryWithMissingSourceJs() throws IOException {
Path scriptDir = tempDir.resolve("no-source");
Files.createDirectories(scriptDir);
Files.writeString(scriptDir.resolve("metadata.json"), "{\"name\":\"Test\"}");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
assertThat(loader.listEntriesForQuery(null)).isEmpty();
}
@Test
void skipsDirectoryWithBlankName() throws IOException {
Path scriptDir = tempDir.resolve("blank-name");
Files.createDirectories(scriptDir);
Files.writeString(scriptDir.resolve("metadata.json"), "{\"name\":\" \"}");
Files.writeString(scriptDir.resolve("source.js"), "/* source */");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
assertThat(loader.listEntriesForQuery(null)).isEmpty();
}
@Test
void filtersByQueryOnNameAndDescription() throws IOException {
Path dir1 = tempDir.resolve("alpha-script");
writeScript(dir1, "Alpha Script", "does alpha things", "/* */");
Path dir2 = tempDir.resolve("beta-script");
writeScript(dir2, "Beta Script", "does beta things", "/* */");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
assertThat(loader.listEntriesForQuery("alpha")).hasSize(1);
assertThat(loader.listEntriesForQuery("beta")).hasSize(1);
assertThat(loader.listEntriesForQuery("script")).hasSize(2);
assertThat(loader.listEntriesForQuery("zzznotfound")).isEmpty();
}
@Test
void findByIdReturnsCorrectScript() throws IOException {
Path scriptDir = tempDir.resolve("find-me");
writeScript(scriptDir, "Find Me", null, "/* */");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
Optional<MarketplaceScriptSeedLoader.SeedScript> found = loader.findById("find-me");
assertThat(found).isPresent();
assertThat(found.get().name()).isEqualTo("Find Me");
}
@Test
void findByIdReturnsEmptyForUnknownId() throws IOException {
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
assertThat(loader.findById("does-not-exist")).isEmpty();
}
@Test
void loadsLogoWhenPresent() throws IOException {
Path scriptDir = tempDir.resolve("with-logo");
writeScript(scriptDir, "With Logo", null, "/* */");
// write a minimal PNG (1x1 white pixel)
byte[] minimalPng = new byte[]{
(byte)0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, (byte)0x90, 0x77, 0x53,
(byte)0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk
0x54, 0x08, (byte)0xd7, 0x63, (byte)0xf8, (byte)0xcf, (byte)0xc0, 0x00,
0x00, 0x00, 0x02, 0x00, 0x01, (byte)0xe2, 0x21, (byte)0xbc,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk
0x44, (byte)0xae, 0x42, 0x60, (byte)0x82
};
Files.write(scriptDir.resolve("logo.png"), minimalPng);
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
Optional<MarketplaceScriptSeedLoader.SeedScript> found = loader.findById("with-logo");
assertThat(found).isPresent();
assertThat(found.get().loadLogo()).isPresent();
assertThat(found.get().entry().logoUrl()).isNotNull();
}
@Test
void normalizesAllowedDomains() throws IOException {
Path scriptDir = tempDir.resolve("domains-script");
Files.createDirectories(scriptDir);
Files.writeString(scriptDir.resolve("metadata.json"),
"{\"name\":\"Domains\",\"allowedDomains\":[\"EXAMPLE.COM\",\"api.foo.bar:8080\",\"duplicate.com\",\"duplicate.com\"]}");
Files.writeString(scriptDir.resolve("source.js"), "/* */");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
Optional<MarketplaceScriptSeedLoader.SeedScript> found = loader.findById("domains-script");
assertThat(found).isPresent();
List<String> domains = found.get().allowedDomains();
assertThat(domains).contains("example.com", "api.foo.bar:8080");
assertThat(domains).doesNotHaveDuplicates();
assertThat(domains).hasSize(3); // EXAMPLE.COM + api.foo.bar:8080 + duplicate.com (deduped)
}
@Test
void loadsSourceContent() throws IOException {
Path scriptDir = tempDir.resolve("source-test");
writeScript(scriptDir, "Source Test", null, "console.log('loaded');");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
Optional<MarketplaceScriptSeedLoader.SeedScript> found = loader.findById("source-test");
assertThat(found).isPresent();
assertThat(found.get().loadSource()).isPresent();
assertThat(new String(found.get().loadSource().get().bytes())).contains("loaded");
}
@Test
void loadsAttachments() throws IOException {
Path scriptDir = tempDir.resolve("with-attachments");
writeScript(scriptDir, "With Attachments", null, "/* */");
Path attachmentsDir = scriptDir.resolve("attachments");
Files.createDirectories(attachmentsDir);
Files.writeString(attachmentsDir.resolve("data.json"), "{\"key\":\"value\"}");
MarketplaceScriptSeedLoader loader = new MarketplaceScriptSeedLoader(tempDir.toString());
Optional<MarketplaceScriptSeedLoader.SeedScript> found = loader.findById("with-attachments");
assertThat(found).isPresent();
assertThat(found.get().attachments()).hasSize(1);
assertThat(found.get().attachments().get(0).name()).isEqualTo("data.json");
}
}
@@ -0,0 +1,26 @@
package dev.kruhlmann.imgfloat.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.Test;
class VersionServiceTest {
@Test
void returnsVersionWhenPomXmlPresent() {
// VersionService reads pom.xml from the filesystem during development;
// in the test classpath pom.xml is available at the project root.
VersionService service = new VersionService();
String version = service.getVersion();
assertThat(version).isNotBlank();
// Should look like a semver string, not "unknown" or empty
assertThat(version).matches("[0-9]+\\.[0-9]+.*");
}
@Test
void versionIsConsistentAcrossCalls() {
VersionService service = new VersionService();
assertThat(service.getVersion()).isEqualTo(service.getVersion());
}
}