Extract ChannelSettingsService for canvas and script-overlay settings; add ChannelSettingsServiceTest

This commit is contained in:
2026-04-21 15:47:25 +02:00
parent 0dd1a7418d
commit 009c8f21fb
5 changed files with 354 additions and 175 deletions
@@ -17,6 +17,7 @@ import dev.kruhlmann.imgfloat.model.api.response.TwitchUserProfile;
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
import dev.kruhlmann.imgfloat.service.AuthorizationService;
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
import dev.kruhlmann.imgfloat.service.ChannelSettingsService;
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
import dev.kruhlmann.imgfloat.util.LogSanitizer;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -60,6 +61,7 @@ public class ChannelApiController {
private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class);
private final ChannelDirectoryService channelDirectoryService;
private final ChannelSettingsService channelSettingsService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
private final TwitchUserLookupService twitchUserLookupService;
@@ -67,12 +69,14 @@ public class ChannelApiController {
public ChannelApiController(
ChannelDirectoryService channelDirectoryService,
ChannelSettingsService channelSettingsService,
OAuth2AuthorizedClientService authorizedClientService,
OAuth2AuthorizedClientRepository authorizedClientRepository,
TwitchUserLookupService twitchUserLookupService,
AuthorizationService authorizationService
) {
this.channelDirectoryService = channelDirectoryService;
this.channelSettingsService = channelSettingsService;
this.authorizedClientService = authorizedClientService;
this.authorizedClientRepository = authorizedClientRepository;
this.twitchUserLookupService = twitchUserLookupService;
@@ -192,7 +196,7 @@ public class ChannelApiController {
@GetMapping("/canvas")
public CanvasSettingsRequest getCanvas(@PathVariable("broadcaster") String broadcaster) {
return channelDirectoryService.getCanvasSettings(broadcaster);
return channelSettingsService.getCanvasSettings(broadcaster);
}
@PutMapping("/canvas")
@@ -212,12 +216,12 @@ public class ChannelApiController {
request.getWidth(),
request.getHeight()
);
return channelDirectoryService.updateCanvasSettings(broadcaster, request, sessionUsername);
return channelSettingsService.updateCanvasSettings(broadcaster, request, sessionUsername);
}
@GetMapping("/settings")
public ChannelScriptSettingsRequest getScriptSettings(@PathVariable("broadcaster") String broadcaster) {
return channelDirectoryService.getChannelScriptSettings(broadcaster);
return channelSettingsService.getChannelScriptSettings(broadcaster);
}
@PutMapping("/settings")
@@ -231,7 +235,7 @@ public class ChannelApiController {
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
LOG.info("Updating script settings for {} by {}", logBroadcaster, logSessionUsername);
return channelDirectoryService.updateChannelScriptSettings(broadcaster, request, sessionUsername);
return channelSettingsService.updateChannelScriptSettings(broadcaster, request, sessionUsername);
}
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@@ -5,8 +5,6 @@ import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
import dev.kruhlmann.imgfloat.model.AssetType;
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
@@ -14,7 +12,6 @@ import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
import dev.kruhlmann.imgfloat.model.api.response.AssetEvent;
import dev.kruhlmann.imgfloat.model.api.response.AssetPatch;
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
import dev.kruhlmann.imgfloat.model.api.response.CanvasEvent;
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
import dev.kruhlmann.imgfloat.model.db.imgfloat.Asset;
@@ -198,162 +195,6 @@ public class ChannelDirectoryService {
.toList();
}
public CanvasSettingsRequest getCanvasSettings(String broadcaster) {
Channel channel = getOrCreateChannel(broadcaster);
return new CanvasSettingsRequest(
channel.getCanvasWidth(),
channel.getCanvasHeight(),
channel.getMaxVolumeDb()
);
}
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req, String actor) {
validateCanvasSettings(req);
Channel channel = getOrCreateChannel(broadcaster);
double beforeWidth = channel.getCanvasWidth();
double beforeHeight = channel.getCanvasHeight();
Double beforeMaxVolumeDb = channel.getMaxVolumeDb();
channel.setCanvasWidth(req.getWidth());
channel.setCanvasHeight(req.getHeight());
if (req.getMaxVolumeDb() != null) {
channel.setMaxVolumeDb(req.getMaxVolumeDb());
}
channelRepository.save(channel);
CanvasSettingsRequest response = new CanvasSettingsRequest(
channel.getCanvasWidth(),
channel.getCanvasHeight(),
channel.getMaxVolumeDb()
);
messagingTemplate.convertAndSend(topicFor(broadcaster), CanvasEvent.updated(broadcaster, response));
if (
beforeWidth != channel.getCanvasWidth() ||
beforeHeight != channel.getCanvasHeight() ||
!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())
) {
List<String> changes = new ArrayList<>();
if (beforeWidth != channel.getCanvasWidth() || beforeHeight != channel.getCanvasHeight()) {
changes.add(
String.format(
Locale.ROOT,
"canvas %.0fx%.0f -> %.0fx%.0f",
beforeWidth,
beforeHeight,
channel.getCanvasWidth(),
channel.getCanvasHeight()
)
);
}
if (!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())) {
changes.add(
String.format(
Locale.ROOT,
"max volume %.0f dB -> %.0f dB",
beforeMaxVolumeDb == null ? 0.0 : beforeMaxVolumeDb,
channel.getMaxVolumeDb() == null ? 0.0 : channel.getMaxVolumeDb()
)
);
}
auditLogService.recordEntry(
channel.getBroadcaster(),
actor,
"CANVAS_UPDATED",
"Canvas settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
);
}
return response;
}
private void validateCanvasSettings(CanvasSettingsRequest req) {
Settings settings = settingsService.get();
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
if (
req.getWidth() <= 0 ||
req.getWidth() > canvasMaxSizePixels ||
!Double.isFinite(req.getWidth()) ||
req.getWidth() % 1 != 0
) throw new ResponseStatusException(
BAD_REQUEST,
"Canvas width must be a whole number within [1 to " + canvasMaxSizePixels + "]"
);
if (
req.getHeight() <= 0 ||
req.getHeight() > canvasMaxSizePixels ||
!Double.isFinite(req.getHeight()) ||
req.getHeight() % 1 != 0
) throw new ResponseStatusException(
BAD_REQUEST,
"Canvas height must be a whole number within [1 to " + canvasMaxSizePixels + "]"
);
if (req.getMaxVolumeDb() != null) {
double maxVolumeDb = req.getMaxVolumeDb();
if (!Double.isFinite(maxVolumeDb) || maxVolumeDb < -60 || maxVolumeDb > 0) {
throw new ResponseStatusException(
BAD_REQUEST,
"Max volume must be within [-60 to 0] dB"
);
}
}
}
public ChannelScriptSettingsRequest getChannelScriptSettings(String broadcaster) {
Channel channel = getOrCreateChannel(broadcaster);
return new ChannelScriptSettingsRequest(
channel.isAllowChannelEmotesForAssets(),
channel.isAllowSevenTvEmotesForAssets(),
channel.isAllowScriptChatAccess()
);
}
public ChannelScriptSettingsRequest updateChannelScriptSettings(
String broadcaster,
ChannelScriptSettingsRequest request,
String actor
) {
Channel channel = getOrCreateChannel(broadcaster);
boolean beforeChannelEmotes = channel.isAllowChannelEmotesForAssets();
boolean beforeSevenTv = channel.isAllowSevenTvEmotesForAssets();
boolean beforeChatAccess = channel.isAllowScriptChatAccess();
channel.setAllowChannelEmotesForAssets(request.isAllowChannelEmotesForAssets());
channel.setAllowSevenTvEmotesForAssets(request.isAllowSevenTvEmotesForAssets());
channel.setAllowScriptChatAccess(request.isAllowScriptChatAccess());
channelRepository.save(channel);
if (
beforeChannelEmotes != channel.isAllowChannelEmotesForAssets() ||
beforeSevenTv != channel.isAllowSevenTvEmotesForAssets() ||
beforeChatAccess != channel.isAllowScriptChatAccess()
) {
List<String> changes = new ArrayList<>();
if (beforeChannelEmotes != channel.isAllowChannelEmotesForAssets()) {
changes.add(
"channelEmotes: " + beforeChannelEmotes + " -> " + channel.isAllowChannelEmotesForAssets()
);
}
if (beforeSevenTv != channel.isAllowSevenTvEmotesForAssets()) {
changes.add(
"sevenTvEmotes: " + beforeSevenTv + " -> " + channel.isAllowSevenTvEmotesForAssets()
);
}
if (beforeChatAccess != channel.isAllowScriptChatAccess()) {
changes.add(
"scriptChatAccess: " + beforeChatAccess + " -> " + channel.isAllowScriptChatAccess()
);
}
String detailSuffix = changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")";
auditLogService.recordEntry(
channel.getBroadcaster(),
actor,
"SCRIPT_SETTINGS_UPDATED",
"Script settings updated" + detailSuffix
);
}
return new ChannelScriptSettingsRequest(
channel.isAllowChannelEmotesForAssets(),
channel.isAllowSevenTvEmotesForAssets(),
channel.isAllowScriptChatAccess()
);
}
@Transactional(rollbackFor = IOException.class)
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file, String actor) throws IOException {
long fileSize = file.getSize();
@@ -0,0 +1,203 @@
package dev.kruhlmann.imgfloat.service;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.response.CanvasEvent;
import dev.kruhlmann.imgfloat.model.db.imgfloat.Channel;
import dev.kruhlmann.imgfloat.model.db.imgfloat.Settings;
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
/**
* Manages per-channel canvas and script-overlay settings.
* Extracted from {@link ChannelDirectoryService} to give it a focused responsibility.
*/
@Service
public class ChannelSettingsService {
private final ChannelRepository channelRepository;
private final SettingsService settingsService;
private final SimpMessagingTemplate messagingTemplate;
private final AuditLogService auditLogService;
public ChannelSettingsService(
ChannelRepository channelRepository,
SettingsService settingsService,
SimpMessagingTemplate messagingTemplate,
AuditLogService auditLogService
) {
this.channelRepository = channelRepository;
this.settingsService = settingsService;
this.messagingTemplate = messagingTemplate;
this.auditLogService = auditLogService;
}
public CanvasSettingsRequest getCanvasSettings(String broadcaster) {
Channel channel = getOrCreateChannel(broadcaster);
return new CanvasSettingsRequest(
channel.getCanvasWidth(),
channel.getCanvasHeight(),
channel.getMaxVolumeDb()
);
}
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req, String actor) {
validateCanvasSettings(req);
Channel channel = getOrCreateChannel(broadcaster);
double beforeWidth = channel.getCanvasWidth();
double beforeHeight = channel.getCanvasHeight();
Double beforeMaxVolumeDb = channel.getMaxVolumeDb();
channel.setCanvasWidth(req.getWidth());
channel.setCanvasHeight(req.getHeight());
if (req.getMaxVolumeDb() != null) {
channel.setMaxVolumeDb(req.getMaxVolumeDb());
}
channelRepository.save(channel);
CanvasSettingsRequest response = new CanvasSettingsRequest(
channel.getCanvasWidth(),
channel.getCanvasHeight(),
channel.getMaxVolumeDb()
);
messagingTemplate.convertAndSend(topicFor(broadcaster), CanvasEvent.updated(broadcaster, response));
boolean changed =
beforeWidth != channel.getCanvasWidth() ||
beforeHeight != channel.getCanvasHeight() ||
!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb());
if (changed) {
List<String> changes = new ArrayList<>();
if (beforeWidth != channel.getCanvasWidth() || beforeHeight != channel.getCanvasHeight()) {
changes.add(
String.format(
Locale.ROOT,
"canvas %.0fx%.0f -> %.0fx%.0f",
beforeWidth,
beforeHeight,
channel.getCanvasWidth(),
channel.getCanvasHeight()
)
);
}
if (!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())) {
changes.add(
String.format(
Locale.ROOT,
"max volume %.0f dB -> %.0f dB",
beforeMaxVolumeDb == null ? 0.0 : beforeMaxVolumeDb,
channel.getMaxVolumeDb() == null ? 0.0 : channel.getMaxVolumeDb()
)
);
}
auditLogService.recordEntry(
channel.getBroadcaster(),
actor,
"CANVAS_UPDATED",
"Canvas settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
);
}
return response;
}
public ChannelScriptSettingsRequest getChannelScriptSettings(String broadcaster) {
Channel channel = getOrCreateChannel(broadcaster);
return new ChannelScriptSettingsRequest(
channel.isAllowChannelEmotesForAssets(),
channel.isAllowSevenTvEmotesForAssets(),
channel.isAllowScriptChatAccess()
);
}
public ChannelScriptSettingsRequest updateChannelScriptSettings(
String broadcaster,
ChannelScriptSettingsRequest request,
String actor
) {
Channel channel = getOrCreateChannel(broadcaster);
boolean beforeChannelEmotes = channel.isAllowChannelEmotesForAssets();
boolean beforeSevenTv = channel.isAllowSevenTvEmotesForAssets();
boolean beforeChatAccess = channel.isAllowScriptChatAccess();
channel.setAllowChannelEmotesForAssets(request.isAllowChannelEmotesForAssets());
channel.setAllowSevenTvEmotesForAssets(request.isAllowSevenTvEmotesForAssets());
channel.setAllowScriptChatAccess(request.isAllowScriptChatAccess());
channelRepository.save(channel);
boolean changed =
beforeChannelEmotes != channel.isAllowChannelEmotesForAssets() ||
beforeSevenTv != channel.isAllowSevenTvEmotesForAssets() ||
beforeChatAccess != channel.isAllowScriptChatAccess();
if (changed) {
List<String> changes = new ArrayList<>();
if (beforeChannelEmotes != channel.isAllowChannelEmotesForAssets()) {
changes.add("channelEmotes: " + beforeChannelEmotes + " -> " + channel.isAllowChannelEmotesForAssets());
}
if (beforeSevenTv != channel.isAllowSevenTvEmotesForAssets()) {
changes.add("sevenTvEmotes: " + beforeSevenTv + " -> " + channel.isAllowSevenTvEmotesForAssets());
}
if (beforeChatAccess != channel.isAllowScriptChatAccess()) {
changes.add("scriptChatAccess: " + beforeChatAccess + " -> " + channel.isAllowScriptChatAccess());
}
auditLogService.recordEntry(
channel.getBroadcaster(),
actor,
"SCRIPT_SETTINGS_UPDATED",
"Script settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
);
}
return new ChannelScriptSettingsRequest(
channel.isAllowChannelEmotesForAssets(),
channel.isAllowSevenTvEmotesForAssets(),
channel.isAllowScriptChatAccess()
);
}
private void validateCanvasSettings(CanvasSettingsRequest req) {
Settings settings = settingsService.get();
int max = settings.getMaxCanvasSideLengthPixels();
if (req.getWidth() <= 0 || req.getWidth() > max || !Double.isFinite(req.getWidth()) || req.getWidth() % 1 != 0) {
throw new ResponseStatusException(
BAD_REQUEST,
"Canvas width must be a whole number within [1 to " + max + "]"
);
}
if (req.getHeight() <= 0 || req.getHeight() > max || !Double.isFinite(req.getHeight()) || req.getHeight() % 1 != 0) {
throw new ResponseStatusException(
BAD_REQUEST,
"Canvas height must be a whole number within [1 to " + max + "]"
);
}
if (req.getMaxVolumeDb() != null) {
double db = req.getMaxVolumeDb();
if (!Double.isFinite(db) || db < -60 || db > 0) {
throw new ResponseStatusException(BAD_REQUEST, "Max volume must be within [-60 to 0] dB");
}
}
}
private Channel getOrCreateChannel(String broadcaster) {
String normalized = normalize(broadcaster);
return channelRepository.findById(normalized)
.orElseGet(() -> channelRepository.save(new Channel(normalized)));
}
private String topicFor(String broadcaster) {
return "/topic/channel/" + normalize(broadcaster);
}
private String normalize(String value) {
return value == null ? null : value.toLowerCase(Locale.ROOT);
}
}
@@ -11,7 +11,6 @@ import static org.mockito.Mockito.when;
import dev.kruhlmann.imgfloat.model.AssetType;
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
@@ -247,17 +246,6 @@ class ChannelDirectoryServiceTest {
assertThat(saved.allowedDomains()).isEmpty();
}
@Test
void updatesCanvasMaxVolumeDb() {
CanvasSettingsRequest request = new CanvasSettingsRequest(1920, 1080, -12.0);
CanvasSettingsRequest saved = service.updateCanvasSettings("caster", request, "caster");
assertThat(saved.getMaxVolumeDb()).isEqualTo(-12.0);
Channel channel = channelRepository.findById("caster").orElseThrow();
assertThat(channel.getMaxVolumeDb()).isEqualTo(-12.0);
}
private byte[] samplePng() throws IOException {
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -0,0 +1,143 @@
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.*;
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
import dev.kruhlmann.imgfloat.model.db.imgfloat.Channel;
import dev.kruhlmann.imgfloat.model.db.imgfloat.Settings;
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
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.web.server.ResponseStatusException;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
class ChannelSettingsServiceTest {
private ChannelRepository channelRepository;
private SettingsService settingsService;
private SimpMessagingTemplate messagingTemplate;
private AuditLogService auditLogService;
private ChannelSettingsService service;
private final ConcurrentHashMap<String, Channel> channels = new ConcurrentHashMap<>();
@BeforeEach
void setup() {
channelRepository = mock(ChannelRepository.class);
settingsService = mock(SettingsService.class);
messagingTemplate = mock(SimpMessagingTemplate.class);
auditLogService = mock(AuditLogService.class);
when(settingsService.get()).thenReturn(Settings.defaults());
when(channelRepository.findById(anyString())).thenAnswer(inv ->
Optional.ofNullable(channels.get(inv.getArgument(0))));
when(channelRepository.save(any(Channel.class))).thenAnswer(inv -> {
Channel ch = inv.getArgument(0);
channels.put(ch.getBroadcaster(), ch);
return ch;
});
service = new ChannelSettingsService(channelRepository, settingsService, messagingTemplate, auditLogService);
}
// --- canvas settings ---
@Test
void getCanvasSettingsReturnsDefaults() {
CanvasSettingsRequest result = service.getCanvasSettings("caster");
assertThat(result.getWidth()).isGreaterThan(0);
assertThat(result.getHeight()).isGreaterThan(0);
}
@Test
void updateCanvasPersistsAndBroadcasts() {
CanvasSettingsRequest req = new CanvasSettingsRequest(1920, 1080, -12.0);
CanvasSettingsRequest result = service.updateCanvasSettings("caster", req, "caster");
assertThat(result.getWidth()).isEqualTo(1920);
assertThat(result.getHeight()).isEqualTo(1080);
assertThat(result.getMaxVolumeDb()).isEqualTo(-12.0);
verify(messagingTemplate).convertAndSend(anyString(), any(Object.class));
Channel channel = channels.get("caster");
assertThat(channel.getMaxVolumeDb()).isEqualTo(-12.0);
}
@Test
void updateCanvasRecordsAuditEntry() {
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
verify(auditLogService).recordEntry(eq("caster"), eq("admin"), eq("CANVAS_UPDATED"), anyString());
}
@Test
void updateCanvasNoAuditEntryWhenNothingChanged() {
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
clearInvocations(auditLogService);
// Second call with same values — nothing changed, no audit
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
verify(auditLogService, never()).recordEntry(any(), any(), any(), any());
}
@Test
void updateCanvasRejectsZeroWidth() {
assertThatThrownBy(() -> service.updateCanvasSettings("caster", new CanvasSettingsRequest(0, 720, null), "admin"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("width");
}
@Test
void updateCanvasRejectsInvalidVolumeDb() {
assertThatThrownBy(() -> service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, 5.0), "admin"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("volume");
}
@Test
void updateCanvasAcceptsBoundaryVolumeValues() {
// -60 and 0 are the inclusive bounds
assertThat(service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, -60.0), "admin")
.getMaxVolumeDb()).isEqualTo(-60.0);
assertThat(service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, 0.0), "admin")
.getMaxVolumeDb()).isEqualTo(0.0);
}
// --- script settings ---
@Test
void getScriptSettingsReturnsDefaults() {
ChannelScriptSettingsRequest result = service.getChannelScriptSettings("caster");
assertThat(result).isNotNull();
}
@Test
void updateScriptSettingsPersistsChanges() {
ChannelScriptSettingsRequest req = new ChannelScriptSettingsRequest(true, false, true);
ChannelScriptSettingsRequest result = service.updateChannelScriptSettings("caster", req, "admin");
assertThat(result.isAllowChannelEmotesForAssets()).isTrue();
assertThat(result.isAllowSevenTvEmotesForAssets()).isFalse();
assertThat(result.isAllowScriptChatAccess()).isTrue();
}
@Test
void updateScriptSettingsRecordsAuditEntry() {
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(true, false, true), "admin");
verify(auditLogService).recordEntry(eq("caster"), eq("admin"), eq("SCRIPT_SETTINGS_UPDATED"), anyString());
}
@Test
void updateScriptSettingsNoAuditWhenNothingChanged() {
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(false, false, false), "admin");
clearInvocations(auditLogService);
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(false, false, false), "admin");
verify(auditLogService, never()).recordEntry(any(), any(), any(), any());
}
}