From 2b43b9a6e019ae82f69e4e5e8d1a71f11973481d Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 18:47:41 +0300 Subject: [PATCH 01/11] Now PlayableItemLoader throws the PlayableItemNotFoundException instead of returning empty mono --- .../PlayerStateUpdatePlayCommandHandlerDelegate.java | 2 -- .../connect/service/player/support/PlayableItemLoader.java | 6 +++++- .../player/support/PredefinedPlayableItemLoader.java | 6 +++--- .../player/support/PredefinedPlayableItemLoaderTest.java | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java index 4677457..8bbb1db 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java @@ -1,6 +1,5 @@ package com.odeyalo.sonata.connect.service.player.handler; -import com.odeyalo.sonata.connect.exception.PlayableItemNotFoundException; import com.odeyalo.sonata.connect.model.CurrentPlayerState; import com.odeyalo.sonata.connect.model.PlayableItem; import com.odeyalo.sonata.connect.model.User; @@ -50,7 +49,6 @@ private Mono executeCommand(@NotNull final PlayCommandContex } return playableItemLoader.loadPlayableItem(playback.getContextUri()) - .switchIfEmpty(Mono.defer(() -> Mono.error(PlayableItemNotFoundException.defaultException()))) .flatMap(item -> play(state, item)); } diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/PlayableItemLoader.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/PlayableItemLoader.java index 076be3f..4ecc327 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/PlayableItemLoader.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/support/PlayableItemLoader.java @@ -1,6 +1,7 @@ package com.odeyalo.sonata.connect.service.player.support; import com.odeyalo.sonata.common.context.ContextUri; +import com.odeyalo.sonata.connect.exception.PlayableItemNotFoundException; import com.odeyalo.sonata.connect.model.PlayableItem; import org.jetbrains.annotations.NotNull; import reactor.core.publisher.Mono; @@ -14,7 +15,10 @@ public interface PlayableItemLoader { * Loads a {@link PlayableItem} based on the provided {@link ContextUri} * * @param contextUri Already parsed contextUri that provide metadata about context-uri string - * @return A {@link Mono} representing the resolved {@link PlayableItem}, or an empty {@link Mono} if no item could be load. + * @return A {@link Mono} representing the resolved {@link PlayableItem}, + * or {@link Mono#error(Throwable)} with {@link PlayableItemNotFoundException} if no item could be load. + * + * @throws PlayableItemNotFoundException - if no playable item associated with provided context uri exist */ @NotNull Mono loadPlayableItem(@NotNull ContextUri contextUri); diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoader.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoader.java index 33b54cf..dacb610 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoader.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoader.java @@ -1,6 +1,7 @@ package com.odeyalo.sonata.connect.service.player.support; import com.odeyalo.sonata.common.context.ContextUri; +import com.odeyalo.sonata.connect.exception.PlayableItemNotFoundException; import com.odeyalo.sonata.connect.model.PlayableItem; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @@ -35,8 +36,7 @@ public PredefinedPlayableItemLoader(final List items) { @Override @NotNull public Mono loadPlayableItem(@NotNull final ContextUri contextUri) { - return Mono.justOrEmpty( - cache.get(contextUri) - ); + return Mono.justOrEmpty(cache.get(contextUri)) + .switchIfEmpty(Mono.defer(() -> Mono.error(PlayableItemNotFoundException.defaultException()))); } } diff --git a/src/test/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoaderTest.java b/src/test/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoaderTest.java index 0c5af9d..9266881 100644 --- a/src/test/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoaderTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/service/player/support/PredefinedPlayableItemLoaderTest.java @@ -1,6 +1,7 @@ package com.odeyalo.sonata.connect.service.player.support; import com.odeyalo.sonata.common.context.ContextUri; +import com.odeyalo.sonata.connect.exception.PlayableItemNotFoundException; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import testing.faker.PlayableItemFaker.TrackItemFaker; @@ -24,11 +25,12 @@ void shouldReturnExistingItemByItsContextUri() { } @Test - void shouldReturnNothingIfItemDoesNotExist() { + void shouldReturnErrorIfNoTrackAssociatedWithProvidedContextUri() { final var testable = new PredefinedPlayableItemLoader(); testable.loadPlayableItem(ContextUri.forTrack("123")) .as(StepVerifier::create) - .verifyComplete(); + .expectError(PlayableItemNotFoundException.class) + .verify(); } } \ No newline at end of file From cd706bfba23c77018e0f90e1a13a1e9989568d58 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 19:21:14 +0300 Subject: [PATCH 02/11] Moved validation of commands to CurrentPlayerState model, removed unused PlayCommandPreExecutingIntegrityValidator.java and its implementations --- .../GlobalExceptionHandlerController.java | 5 +++ .../connect/model/CurrentPlayerState.java | 13 +++++++ ...StateUpdatePlayCommandHandlerDelegate.java | 11 ------ ...CommandPreExecutingIntegrityValidator.java | 39 ------------------- ...CommandPreExecutingIntegrityValidator.java | 22 ----------- ...layerCommandIntegrityValidationResult.java | 34 ---------------- .../PlayResumePlaybackEndpointTest.java | 1 - ...entPlayerStateProgressCalculationTest.java | 38 ++++++++++++++---- .../connect/model/SeekToPositionTest.java | 29 ++++++++++---- ...efaultPlayerOperationsTestableBuilder.java | 2 - 10 files changed, 71 insertions(+), 123 deletions(-) delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardcodedPlayCommandPreExecutingIntegrityValidator.java delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayCommandPreExecutingIntegrityValidator.java delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayerCommandIntegrityValidationResult.java diff --git a/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java b/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java index 5609155..b5c793e 100644 --- a/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java +++ b/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java @@ -78,6 +78,11 @@ public ResponseEntity handleMissingRequestParameterException(final MissingReq .body(ExceptionMessage.of(ex.getMessage())); } + @ExceptionHandler(IllegalCommandStateException.class) + public ResponseEntity handleIllegalCommandStateException(IllegalCommandStateException ex) { + return badRequest().body(ReasonCodeAwareExceptionMessage.of(ex.getReasonCode(), "Player command failed: Nothing is playing now and context is null!")); + } + @ExceptionHandler(MissingPlayableItemException.class) public ResponseEntity handleMissingPlayableItemException(final MissingPlayableItemException ex) { final var body = ReasonCodeAwareExceptionMessage.of(ex.getReasonCode(), "Player command error: no item is playing"); diff --git a/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java b/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java index b5f8c6f..2427370 100644 --- a/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java +++ b/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java @@ -1,6 +1,8 @@ package com.odeyalo.sonata.connect.model; +import com.odeyalo.sonata.connect.exception.IllegalCommandStateException; import com.odeyalo.sonata.connect.exception.MissingPlayableItemException; +import com.odeyalo.sonata.connect.exception.NoActiveDeviceException; import com.odeyalo.sonata.connect.exception.SeekPositionExceedDurationException; import com.odeyalo.sonata.connect.service.player.SeekPosition; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevice; @@ -10,6 +12,7 @@ import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import reactor.core.publisher.Mono; import java.util.Random; @@ -153,6 +156,11 @@ public CurrentPlayerState transferPlayback(@NotNull final TargetDevice deviceToT @NotNull public CurrentPlayerState play(@NotNull final PlayableItem item) { + + if ( missingActiveDevice() ) { + throw NoActiveDeviceException.defaultException(); + } + return this.toBuilder() .playing(true) .playableItem(item) @@ -164,6 +172,11 @@ public CurrentPlayerState play(@NotNull final PlayableItem item) { @NotNull public CurrentPlayerState resumePlayback() { + + if ( missingPlayingItem() ) { + throw IllegalCommandStateException.withCustomMessage("Player command failed: Nothing is playing now and context is null!"); + } + return this.toBuilder() .playing(true) .playStartTime(clock.currentTimeMillis()) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java index 8bbb1db..9c57acc 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePlayCommandHandlerDelegate.java @@ -7,7 +7,6 @@ import com.odeyalo.sonata.connect.service.player.PlayerStateService; import com.odeyalo.sonata.connect.service.player.TargetDevice; import com.odeyalo.sonata.connect.service.player.support.PlayableItemLoader; -import com.odeyalo.sonata.connect.service.player.support.validation.PlayCommandPreExecutingIntegrityValidator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; @@ -20,13 +19,10 @@ public class PlayerStateUpdatePlayCommandHandlerDelegate implements PlayCommandHandlerDelegate { private final PlayerStateService playerStateService; private final PlayableItemLoader playableItemLoader; - private final PlayCommandPreExecutingIntegrityValidator integrityValidator; public PlayerStateUpdatePlayCommandHandlerDelegate(final PlayableItemLoader playableItemLoader, - final PlayCommandPreExecutingIntegrityValidator integrityValidator, final PlayerStateService playerStateService) { this.playableItemLoader = playableItemLoader; - this.integrityValidator = integrityValidator; this.playerStateService = playerStateService; } @@ -36,7 +32,6 @@ public Mono playOrResume(@NotNull final User user, @NotNull final PlayCommandContext context, @Nullable final TargetDevice targetDevice) { return playerStateService.loadPlayerState(user) - .flatMap(state -> validateCommand(context, state)) .flatMap(state -> executeCommand(context, state)); } @@ -69,10 +64,4 @@ private Mono resumePlayback(@NotNull final CurrentPlayerStat ); } - @NotNull - private Mono validateCommand(@NotNull final PlayCommandContext context, - @NotNull final CurrentPlayerState state) { - return integrityValidator.validate(context, state) - .thenReturn(state); - } } diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardcodedPlayCommandPreExecutingIntegrityValidator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardcodedPlayCommandPreExecutingIntegrityValidator.java deleted file mode 100644 index b15e8d5..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardcodedPlayCommandPreExecutingIntegrityValidator.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.support.validation; - -import com.odeyalo.sonata.connect.exception.IllegalCommandStateException; -import com.odeyalo.sonata.connect.exception.NoActiveDeviceException; -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.service.player.PlayCommandContext; -import org.jetbrains.annotations.NotNull; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -/** - * PlayCommandPreExecutingIntegrityValidator implementation that hardcoded to written conditions in class. - *

- * - * Rules applied: - * - at least one connected device should present - * - if {@link PlayCommandContext} missing a {@link com.odeyalo.sonata.common.context.ContextUri} then a {@link CurrentPlayerState} should contain playable item - *

- * It can be used in tests - */ -@Component -public final class HardcodedPlayCommandPreExecutingIntegrityValidator implements PlayCommandPreExecutingIntegrityValidator { - - @Override - @NotNull - public Mono validate(@NotNull final PlayCommandContext playback, - @NotNull final CurrentPlayerState playerState) { - - if ( playerState.missingActiveDevice() ) { - return Mono.error(NoActiveDeviceException::defaultException); - } - - if ( playback.shouldBeResumed() && playerState.missingPlayingItem() ) { - return Mono.error(() -> IllegalCommandStateException.withCustomMessage("Player command failed: Nothing is playing now and context is null!")); - } - - return Mono.empty(); - } -} diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayCommandPreExecutingIntegrityValidator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayCommandPreExecutingIntegrityValidator.java deleted file mode 100644 index 451d23d..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayCommandPreExecutingIntegrityValidator.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.support.validation; - -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.service.player.PlayCommandContext; -import org.jetbrains.annotations.NotNull; -import reactor.core.publisher.Mono; - -/** - * Contract to validate play or resume command before it is being executed - */ -public interface PlayCommandPreExecutingIntegrityValidator { - /** - * Validate the given arguments before executing play or resume playback command - * - * @param context - a play command context that contains info about command - * @param currentState - current state associated with user, before executing this command - * @return - {@link Mono} with the {@link Void} on success or {@link Mono#error(Throwable) } with an error that occurred - */ - @NotNull - Mono validate(@NotNull PlayCommandContext context, - @NotNull CurrentPlayerState currentState); -} diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayerCommandIntegrityValidationResult.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayerCommandIntegrityValidationResult.java deleted file mode 100644 index f127135..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PlayerCommandIntegrityValidationResult.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.support.validation; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; - -/** - * A simple result wrapper about play command integrity validation status. - */ -@Value -@AllArgsConstructor(staticName = "of") -@Builder -public class PlayerCommandIntegrityValidationResult { - boolean valid; - // Contains info why this play command is invalid - Throwable occurredException; - - /** - * Create passed result and return it - * @return - passed result - */ - public static PlayerCommandIntegrityValidationResult valid() { - return of(true, null); - } - - /** - * Create result about command that don't pass - * @param ex - exception to wrap and return - * @return - wrapped failed result with exception - */ - public static PlayerCommandIntegrityValidationResult invalid(Throwable ex) { - return of(false, ex); - } -} diff --git a/src/test/java/com/odeyalo/sonata/connect/controller/PlayResumePlaybackEndpointTest.java b/src/test/java/com/odeyalo/sonata/connect/controller/PlayResumePlaybackEndpointTest.java index 326a68f..e1d37ac 100644 --- a/src/test/java/com/odeyalo/sonata/connect/controller/PlayResumePlaybackEndpointTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/controller/PlayResumePlaybackEndpointTest.java @@ -51,7 +51,6 @@ public class PlayResumePlaybackEndpointTest { PlayerStateRepository playerStateRepository; @Autowired - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") SonataTestHttpOperations testOperations; final String EXISTING_TRACK_CONTEXT_URI = "sonata:track:cassie"; diff --git a/src/test/java/com/odeyalo/sonata/connect/model/CurrentPlayerStateProgressCalculationTest.java b/src/test/java/com/odeyalo/sonata/connect/model/CurrentPlayerStateProgressCalculationTest.java index 33d2155..948bacd 100644 --- a/src/test/java/com/odeyalo/sonata/connect/model/CurrentPlayerStateProgressCalculationTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/model/CurrentPlayerStateProgressCalculationTest.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.time.Instant; +import static com.odeyalo.sonata.connect.model.DeviceSpec.DeviceStatus.ACTIVE; import static org.assertj.core.api.Assertions.assertThat; class CurrentPlayerStateProgressCalculationTest { @@ -16,6 +17,14 @@ class CurrentPlayerStateProgressCalculationTest { static final User USER = User.of("odeyalooo"); + static final Device DEVICE = Device.builder() + .deviceId("miku") + .deviceName("Odeyalo-PC") + .deviceType(DeviceType.COMPUTER) + .status(ACTIVE) + .volume(Volume.fromInt(35)) + .build(); + @Test void shouldReturnDefaultValueIfNothingIsPlaying() { final CurrentPlayerState testable = CurrentPlayerState.emptyFor(USER) @@ -28,7 +37,9 @@ void shouldReturnDefaultValueIfNothingIsPlaying() { void shouldNotReturnDefaultValueIfSomethingIsPlaying() { final CurrentPlayerState initialState = CurrentPlayerState.emptyFor(USER); - final CurrentPlayerState updatedState = initialState.play(SECONDS_30_PLAYABLE_ITEM); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState updatedState = withConnectedDevice.play(SECONDS_30_PLAYABLE_ITEM); assertThat(updatedState.getProgressMs()).isNotEqualTo(-1L); } @@ -54,7 +65,9 @@ void shouldReturnCurrentProgressForItemThatPlayingNow() { final CurrentPlayerState initialState = CurrentPlayerState.emptyFor(USER) .useClock(clock); - final CurrentPlayerState testable = initialState.play(SECONDS_30_PLAYABLE_ITEM); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(SECONDS_30_PLAYABLE_ITEM); clock.waitSeconds(2); @@ -73,7 +86,9 @@ void shouldReturnProgressMsAfterThePauseCommand() { final CurrentPlayerState initialState = CurrentPlayerState.emptyFor(USER) .useClock(clock); - final CurrentPlayerState testable = initialState.play(SECONDS_30_PLAYABLE_ITEM); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(SECONDS_30_PLAYABLE_ITEM); clock.waitSeconds(6); @@ -94,7 +109,9 @@ void shouldContinueProgressMsAfterThePointPlaybackWasPaused() { final CurrentPlayerState initialState = CurrentPlayerState.emptyFor(USER) .useClock(clock); - final CurrentPlayerState testable = initialState.play(SECONDS_30_PLAYABLE_ITEM); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(SECONDS_30_PLAYABLE_ITEM); clock.waitSeconds(6); @@ -117,7 +134,9 @@ void shouldProperlyCalculateTheProgressIfMillisPassed() { final CurrentPlayerState initialState = CurrentPlayerState.emptyFor(USER) .useClock(clock); - final CurrentPlayerState testable = initialState.play(SECONDS_30_PLAYABLE_ITEM); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(SECONDS_30_PLAYABLE_ITEM); clock.waitMillis(60); @@ -135,7 +154,10 @@ void shouldReturnEndOfTheProgressIfProgressIsOutOfBoundsOfPlayableItem() { .setDuration(Duration.ofSeconds(230)) .get(); - final CurrentPlayerState testable = initialState.play(playableItem); + + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(playableItem); clock.waitSeconds(240); @@ -153,7 +175,9 @@ void shouldReturnEndOfTheProgressIfProgressIsEqualToItemDuration() { .setDuration(Duration.ofSeconds(230)) .get(); - final CurrentPlayerState testable = initialState.play(playableItem); + final CurrentPlayerState withConnectedDevice = initialState.connectDevice(DEVICE); + + final CurrentPlayerState testable = withConnectedDevice.play(playableItem); clock.waitSeconds(230); diff --git a/src/test/java/com/odeyalo/sonata/connect/model/SeekToPositionTest.java b/src/test/java/com/odeyalo/sonata/connect/model/SeekToPositionTest.java index 0795eb2..a60f0c3 100644 --- a/src/test/java/com/odeyalo/sonata/connect/model/SeekToPositionTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/model/SeekToPositionTest.java @@ -10,24 +10,36 @@ import java.time.Duration; import java.time.Instant; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static com.odeyalo.sonata.connect.model.DeviceSpec.DeviceStatus.ACTIVE; +import static org.assertj.core.api.Assertions.*; public final class SeekToPositionTest { - public static final PlayableItem SIMPLE_TRACK = PlayableItemFaker.create() + static final PlayableItem SIMPLE_TRACK = PlayableItemFaker.create() .setDuration(Duration.ofSeconds(200)) .get(); - public static final User USER = User.of("123"); + + static final User USER = User.of("123"); + + static final Device DEVICE = Device.builder() + .deviceId("miku") + .deviceName("Odeyalo-PC") + .deviceType(DeviceType.COMPUTER) + .status(ACTIVE) + .volume(Volume.fromInt(35)) + .build(); + @Test void shouldProperlySeekPlayerProgressToPosition() { final TestingClock timer = new TestingClock(Instant.now()); - final CurrentPlayerState initialPlayer = CurrentPlayerState.emptyFor(User.of("123")) + final CurrentPlayerState initialPlayer = CurrentPlayerState.emptyFor(USER) .useClock(timer); - final CurrentPlayerState afterPlay = initialPlayer.play(SIMPLE_TRACK); + final CurrentPlayerState withConnectedDevices = initialPlayer.connectDevice(DEVICE); + + final CurrentPlayerState afterPlay = withConnectedDevices.play(SIMPLE_TRACK); timer.waitSeconds(5); @@ -40,7 +52,10 @@ void shouldProperlySeekPlayerProgressToPosition() { void shouldThrowExceptionIfSeekPositionIsGreaterThanTrackDuration() { final CurrentPlayerState initialPlayer = CurrentPlayerState.emptyFor(USER); - final CurrentPlayerState afterPlay = initialPlayer.play(SIMPLE_TRACK); + final CurrentPlayerState withConnectedDevices = initialPlayer.connectDevice(DEVICE); + + final CurrentPlayerState afterPlay = withConnectedDevices.play(SIMPLE_TRACK); + assertThatThrownBy(() -> afterPlay.seekTo(SeekPosition.ofMillis(Integer.MAX_VALUE))) .isInstanceOf(SeekPositionExceedDurationException.class); diff --git a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java index 064e190..806209e 100644 --- a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java +++ b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java @@ -17,7 +17,6 @@ import com.odeyalo.sonata.connect.service.player.support.PlayableItemLoader; import com.odeyalo.sonata.connect.service.player.support.PredefinedPlayableItemLoader; import com.odeyalo.sonata.connect.service.player.support.validation.HardCodedPauseCommandPreExecutingIntegrityValidator; -import com.odeyalo.sonata.connect.service.player.support.validation.HardcodedPlayCommandPreExecutingIntegrityValidator; import com.odeyalo.sonata.connect.service.support.mapper.CurrentPlayerState2CurrentlyPlayingPlayerStateConverter; import com.odeyalo.sonata.connect.service.support.mapper.PlayerState2CurrentPlayerStateConverter; import org.jetbrains.annotations.NotNull; @@ -89,7 +88,6 @@ public PlayCommandHandlerBuilder withPlayableItems(final List exis public PlayCommandHandlerDelegate build() { return new PlayerStateUpdatePlayCommandHandlerDelegate( itemLoader, - new HardcodedPlayCommandPreExecutingIntegrityValidator(), new PlayerStateService(playerStateRepository, testableBuilder().playerStateConverterSupport, new DefaultPlayerStateEntityFactory( new DeviceEntity.Factory(), new TrackItemEntity.Factory() ))); From 2604e36d8c494e089bfb90f030933dec57c2bf75 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 19:29:18 +0300 Subject: [PATCH 03/11] Moved validation of pause command to CurrentPlayerState model, removed unused PauseCommandPreExecutingIntegrityValidator.java and its implementations --- .../connect/model/CurrentPlayerState.java | 6 ++++- ...tateUpdatePauseCommandHandlerDelegate.java | 14 +++--------- ...CommandPreExecutingIntegrityValidator.java | 22 ------------------- ...CommandPreExecutingIntegrityValidator.java | 19 ---------------- .../controller/PauseCommandEndpointTest.java | 6 +++-- ...efaultPlayerOperationsTestableBuilder.java | 2 -- 6 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardCodedPauseCommandPreExecutingIntegrityValidator.java delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PauseCommandPreExecutingIntegrityValidator.java diff --git a/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java b/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java index 2427370..0c23f2a 100644 --- a/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java +++ b/src/main/java/com/odeyalo/sonata/connect/model/CurrentPlayerState.java @@ -12,7 +12,6 @@ import lombok.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import reactor.core.publisher.Mono; import java.util.Random; @@ -185,6 +184,11 @@ public CurrentPlayerState resumePlayback() { @NotNull public CurrentPlayerState pause() { + + if ( missingActiveDevice() ) { + throw NoActiveDeviceException.defaultException(); + } + if ( isPlaying() ) { return this.toBuilder() .playing(false) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java index d39ce65..1fd3f19 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java @@ -3,30 +3,22 @@ import com.odeyalo.sonata.connect.model.CurrentPlayerState; import com.odeyalo.sonata.connect.model.User; import com.odeyalo.sonata.connect.service.player.PlayerStateService; -import com.odeyalo.sonata.connect.service.player.support.validation.PauseCommandPreExecutingIntegrityValidator; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component -@RequiredArgsConstructor +@AllArgsConstructor public final class PlayerStateUpdatePauseCommandHandlerDelegate implements PauseCommandHandlerDelegate { - private final PauseCommandPreExecutingIntegrityValidator preExecutingIntegrityValidator; private final PlayerStateService playerStateService; + @Override @NotNull public Mono pause(@NotNull final User user) { return playerStateService.loadPlayerState(user) - .flatMap(this::validateCommand) .map(CurrentPlayerState::pause) .flatMap(playerStateService::save); } - - @NotNull - private Mono validateCommand(@NotNull final CurrentPlayerState state) { - return preExecutingIntegrityValidator.validate(state) - .thenReturn(state); - } } diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardCodedPauseCommandPreExecutingIntegrityValidator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardCodedPauseCommandPreExecutingIntegrityValidator.java deleted file mode 100644 index 101078e..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/HardCodedPauseCommandPreExecutingIntegrityValidator.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.support.validation; - -import com.odeyalo.sonata.connect.exception.NoActiveDeviceException; -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import org.jetbrains.annotations.NotNull; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -@Component -public final class HardCodedPauseCommandPreExecutingIntegrityValidator implements PauseCommandPreExecutingIntegrityValidator { - - @Override - @NotNull - public Mono validate(@NotNull final CurrentPlayerState currentState) { - - if ( currentState.missingActiveDevice() ) { - return Mono.error(NoActiveDeviceException::defaultException); - } - - return Mono.empty(); - } -} diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PauseCommandPreExecutingIntegrityValidator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PauseCommandPreExecutingIntegrityValidator.java deleted file mode 100644 index dbad9d1..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/support/validation/PauseCommandPreExecutingIntegrityValidator.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.support.validation; - -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import org.jetbrains.annotations.NotNull; -import reactor.core.publisher.Mono; - -/** - * Contract to validate pause command before its being executes - */ -public interface PauseCommandPreExecutingIntegrityValidator { - /** - * Validate the state before executing the pause command - * @param currentState - current state associated with user - * @return - {@link Mono} with {@link Void} on success, or {@link Mono#error(Throwable)} with an error - */ - @NotNull - Mono validate(@NotNull CurrentPlayerState currentState); - -} diff --git a/src/test/java/com/odeyalo/sonata/connect/controller/PauseCommandEndpointTest.java b/src/test/java/com/odeyalo/sonata/connect/controller/PauseCommandEndpointTest.java index 23ae9dc..2b1024a 100644 --- a/src/test/java/com/odeyalo/sonata/connect/controller/PauseCommandEndpointTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/controller/PauseCommandEndpointTest.java @@ -6,7 +6,10 @@ import com.odeyalo.sonata.connect.dto.ReasonCodeAwareExceptionMessage; import com.odeyalo.sonata.connect.repository.PlayerStateRepository; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; @@ -36,7 +39,6 @@ class PauseCommandEndpointTest { WebTestClient webTestClient; @Autowired PlayerStateRepository playerStateRepository; - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired SonataTestHttpOperations sonataTestHttpOperations; diff --git a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java index 806209e..ce876f2 100644 --- a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java +++ b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java @@ -16,7 +16,6 @@ import com.odeyalo.sonata.connect.service.player.handler.PlayerStateUpdatePlayCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.player.support.PlayableItemLoader; import com.odeyalo.sonata.connect.service.player.support.PredefinedPlayableItemLoader; -import com.odeyalo.sonata.connect.service.player.support.validation.HardCodedPauseCommandPreExecutingIntegrityValidator; import com.odeyalo.sonata.connect.service.support.mapper.CurrentPlayerState2CurrentlyPlayingPlayerStateConverter; import com.odeyalo.sonata.connect.service.support.mapper.PlayerState2CurrentPlayerStateConverter; import org.jetbrains.annotations.NotNull; @@ -32,7 +31,6 @@ public final class DefaultPlayerOperationsTestableBuilder { private final PauseCommandHandlerDelegate pauseCommandHandlerDelegate = new PlayerStateUpdatePauseCommandHandlerDelegate( - new HardCodedPauseCommandPreExecutingIntegrityValidator(), new PlayerStateService(playerStateRepository, playerStateConverterSupport, new DefaultPlayerStateEntityFactory( new DeviceEntity.Factory(), new TrackItemEntity.Factory() ))); From 4381a906db6536096ada0b39d6efe8644d1fcf9d Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 19:36:44 +0300 Subject: [PATCH 04/11] Removed PauseCommandHandlerDelegate.java as unused, moved logic closer to DefaultPlayerOperations --- .../player/DefaultPlayerOperations.java | 10 ++++---- .../handler/PauseCommandHandlerDelegate.java | 13 ---------- ...tateUpdatePauseCommandHandlerDelegate.java | 24 ------------------- ...efaultPlayerOperationsTestableBuilder.java | 9 ------- .../faker/CurrentPlayerStateFaker.java | 4 +++- 5 files changed, 8 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/handler/PauseCommandHandlerDelegate.java delete mode 100644 src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultPlayerOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultPlayerOperations.java index a7e655d..aaca83f 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultPlayerOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultPlayerOperations.java @@ -1,10 +1,9 @@ package com.odeyalo.sonata.connect.service.player; import com.odeyalo.sonata.connect.model.*; -import com.odeyalo.sonata.connect.service.player.handler.PauseCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.player.handler.PlayCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.support.mapper.CurrentPlayerState2CurrentlyPlayingPlayerStateConverter; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -15,10 +14,9 @@ import static reactor.core.publisher.Mono.defer; @Component -@RequiredArgsConstructor +@AllArgsConstructor public final class DefaultPlayerOperations implements BasicPlayerOperations { private final PlayCommandHandlerDelegate playCommandHandlerDelegate; - private final PauseCommandHandlerDelegate pauseCommandHandlerDelegate; private final CurrentPlayerState2CurrentlyPlayingPlayerStateConverter playerStateConverter; private final PlayerStateService playerStateService; @@ -59,7 +57,9 @@ public Mono playOrResume(@NotNull final User user, @Override @NotNull public Mono pause(@NotNull User user) { - return pauseCommandHandlerDelegate.pause(user); + return playerStateService.loadPlayerState(user) + .map(CurrentPlayerState::pause) + .flatMap(playerStateService::save); } @Override diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PauseCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PauseCommandHandlerDelegate.java deleted file mode 100644 index dc61e8e..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PauseCommandHandlerDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.handler; - -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.model.User; -import org.jetbrains.annotations.NotNull; -import reactor.core.publisher.Mono; - -public interface PauseCommandHandlerDelegate { - - @NotNull - Mono pause(@NotNull User user); - -} diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java deleted file mode 100644 index 1fd3f19..0000000 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/PlayerStateUpdatePauseCommandHandlerDelegate.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.odeyalo.sonata.connect.service.player.handler; - -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.model.User; -import com.odeyalo.sonata.connect.service.player.PlayerStateService; -import lombok.AllArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -@Component -@AllArgsConstructor -public final class PlayerStateUpdatePauseCommandHandlerDelegate implements PauseCommandHandlerDelegate { - private final PlayerStateService playerStateService; - - - @Override - @NotNull - public Mono pause(@NotNull final User user) { - return playerStateService.loadPlayerState(user) - .map(CurrentPlayerState::pause) - .flatMap(playerStateService::save); - } -} diff --git a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java index ce876f2..efb3e45 100644 --- a/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java +++ b/src/test/java/testing/factory/DefaultPlayerOperationsTestableBuilder.java @@ -10,9 +10,7 @@ import com.odeyalo.sonata.connect.repository.PlayerStateRepository; import com.odeyalo.sonata.connect.service.player.DefaultPlayerOperations; import com.odeyalo.sonata.connect.service.player.PlayerStateService; -import com.odeyalo.sonata.connect.service.player.handler.PauseCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.player.handler.PlayCommandHandlerDelegate; -import com.odeyalo.sonata.connect.service.player.handler.PlayerStateUpdatePauseCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.player.handler.PlayerStateUpdatePlayCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.player.support.PlayableItemLoader; import com.odeyalo.sonata.connect.service.player.support.PredefinedPlayableItemLoader; @@ -29,12 +27,6 @@ public final class DefaultPlayerOperationsTestableBuilder { private final PlayerState2CurrentPlayerStateConverter playerStateConverterSupport = new Converters().playerState2CurrentPlayerStateConverter(); private final CurrentPlayerState2CurrentlyPlayingPlayerStateConverter playerStateConverter = new Converters().currentPlayerStateConverter(); - private final PauseCommandHandlerDelegate pauseCommandHandlerDelegate = - new PlayerStateUpdatePauseCommandHandlerDelegate( - new PlayerStateService(playerStateRepository, playerStateConverterSupport, new DefaultPlayerStateEntityFactory( - new DeviceEntity.Factory(), new TrackItemEntity.Factory() - ))); - public static DefaultPlayerOperationsTestableBuilder testableBuilder() { return new DefaultPlayerOperationsTestableBuilder(); } @@ -57,7 +49,6 @@ public DefaultPlayerOperations build() { .withState(playerStateRepository) .withPlayableItems(existingItems) .build(), - pauseCommandHandlerDelegate, playerStateConverter, new PlayerStateService(playerStateRepository, playerStateConverterSupport, new DefaultPlayerStateEntityFactory(new DeviceEntity.Factory(), new TrackItemEntity.Factory()))); diff --git a/src/test/java/testing/faker/CurrentPlayerStateFaker.java b/src/test/java/testing/faker/CurrentPlayerStateFaker.java index dee94a6..abaaa7b 100644 --- a/src/test/java/testing/faker/CurrentPlayerStateFaker.java +++ b/src/test/java/testing/faker/CurrentPlayerStateFaker.java @@ -5,6 +5,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.jetbrains.annotations.NotNull; +import java.time.Duration; import java.util.Collections; public final class CurrentPlayerStateFaker { @@ -12,7 +13,8 @@ public final class CurrentPlayerStateFaker { Faker faker = Faker.instance(); public CurrentPlayerStateFaker() { - PlayableItem playableItem = PlayableItemFaker.create().get(); + Integer seconds = faker.random().nextInt(100, 500); + PlayableItem playableItem = PlayableItemFaker.create().setDuration(Duration.ofSeconds(seconds)).get(); builder.playableItem(playableItem) .playingType(PlayingType.TRACK) .repeatState(faker.options().option(RepeatState.class)) From 97b277797e753081cb02828f5433a3b37d6d346b Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 19:47:48 +0300 Subject: [PATCH 05/11] Refactor of SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate, removed unnecessary if statement --- .../java/com/odeyalo/sonata/connect/model/Devices.java | 2 +- ...DeviceOnlyTransferPlaybackCommandHandlerDelegate.java | 9 ++------- ...ceOnlyTransferPlaybackCommandHandlerDelegateTest.java | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/odeyalo/sonata/connect/model/Devices.java b/src/main/java/com/odeyalo/sonata/connect/model/Devices.java index cce0f4b..5b9da21 100644 --- a/src/main/java/com/odeyalo/sonata/connect/model/Devices.java +++ b/src/main/java/com/odeyalo/sonata/connect/model/Devices.java @@ -147,7 +147,7 @@ public Devices changeVolume(@NotNull final Volume volume) { @NotNull private Device findDeviceToActivate(@NotNull final TargetDevice searchTarget) { return findById(searchTarget) - .orElseThrow(() -> DeviceNotFoundException.withCustomMessage(String.format("Device with ID: %s not found", searchTarget.getId()))); + .orElseThrow(() -> DeviceNotFoundException.withCustomMessage(String.format("Device with ID: %s not found!", searchTarget.getId()))); } @Nullable diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java index bd63c52..8e2f02d 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java @@ -1,6 +1,5 @@ package com.odeyalo.sonata.connect.service.player.handler; -import com.odeyalo.sonata.connect.exception.DeviceNotFoundException; import com.odeyalo.sonata.connect.exception.MultipleTargetDevicesNotSupportedException; import com.odeyalo.sonata.connect.exception.SingleTargetDeactivationDeviceRequiredException; import com.odeyalo.sonata.connect.exception.TargetDeviceRequiredException; @@ -51,11 +50,7 @@ private Mono delegateTransferPlayback(@NotNull final TargetD final TargetDevice transferPlaybackTarget = targetDevices.peekFirst(); - if ( playerState.hasDevice(transferPlaybackTarget) ) { - return doTransferPlayback(playerState, transferPlaybackTarget); - } - - return Mono.error(DeviceNotFoundException.defaultException()); + return doTransferPlayback(playerState, transferPlaybackTarget); } @NotNull @@ -87,7 +82,7 @@ private static Pair validate(final TargetDevices targetDevic return Pair.of(false, SingleTargetDeactivationDeviceRequiredException.defaultException()); } - if ( targetDevices.size() < 1 ) { + if ( targetDevices.isEmpty() ) { return Pair.of(false, TargetDeviceRequiredException.defaultException()); } diff --git a/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java b/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java index 873a84b..d84ebb8 100644 --- a/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java @@ -95,7 +95,7 @@ void shouldThrowExceptionIfDeviceIdIsInvalid() { .expectErrorSatisfies(err -> { assertThat(err) .isInstanceOf(DeviceNotFoundException.class) - .hasMessage("Device with provided ID was not found!") + .hasMessage("Device with ID: not_exist not found!") .is(reasonCodeEqual("device_not_found")); }) .verify(); From eebe03591ba22050236131313ead4cf4a704e094 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Mon, 26 Aug 2024 20:34:34 +0300 Subject: [PATCH 06/11] Removed unused method 'containsById' in DeviceOperations, renamed DefaultStorageDeviceOperations to DefaultDeviceOperations --- ...ions.java => DefaultDeviceOperations.java} | 19 ++++++++----------- .../service/player/DeviceOperations.java | 3 --- ...entPublisherDeviceOperationsDecorator.java | 6 ------ ....java => DefaultDeviceOperationsTest.java} | 4 ++-- .../testing/stub/NullDeviceOperations.java | 6 ------ 5 files changed, 10 insertions(+), 28 deletions(-) rename src/main/java/com/odeyalo/sonata/connect/service/player/{DefaultStorageDeviceOperations.java => DefaultDeviceOperations.java} (76%) rename src/test/java/com/odeyalo/sonata/connect/service/player/{DefaultStorageDeviceOperationsTest.java => DefaultDeviceOperationsTest.java} (96%) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java similarity index 76% rename from src/main/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperations.java rename to src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java index 9c8c72d..8d536b1 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java @@ -12,13 +12,13 @@ import reactor.core.publisher.Mono; @Component -public class DefaultStorageDeviceOperations implements DeviceOperations { - private final TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate; +public class DefaultDeviceOperations implements DeviceOperations { private final PlayerStateService playerStateService; + private final TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate; @Autowired - public DefaultStorageDeviceOperations(PlayerStateService playerStateService, - TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate) { + public DefaultDeviceOperations(PlayerStateService playerStateService, + TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate) { this.playerStateService = playerStateService; this.transferPlaybackCommandHandlerDelegate = transferPlaybackCommandHandlerDelegate; } @@ -35,13 +35,10 @@ public Mono addDevice(@NotNull final User user, @NotNull @Override - public Mono containsById(User user, String deviceId) { - return Mono.empty(); - } - - @NotNull - @Override - public Mono transferPlayback(User user, SwitchDeviceCommandArgs args, TargetDeactivationDevices deactivationDevices, TargetDevices targetDevices) { + public Mono transferPlayback(@NotNull final User user, + @NotNull final SwitchDeviceCommandArgs args, + @NotNull final TargetDeactivationDevices deactivationDevices, + @NotNull final TargetDevices targetDevices) { return transferPlaybackCommandHandlerDelegate.transferPlayback(user, args, deactivationDevices, targetDevices); } diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java index 02364e1..c246a55 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java @@ -16,9 +16,6 @@ public interface DeviceOperations { @NotNull Mono addDevice(User user, Device device); - @NotNull - Mono containsById(User user, String deviceId); - /** * Transfer the playback to given devices * @param user - user that owns player state diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java index f85fa9e..5b9a36c 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java @@ -36,12 +36,6 @@ public Mono addDevice(User user, Device device) { } - @NotNull - @Override - public Mono containsById(User user, String deviceId) { - return delegate.containsById(user, deviceId); - } - @NotNull @Override public Mono transferPlayback(User user, SwitchDeviceCommandArgs args, TargetDeactivationDevices deactivationDevices, TargetDevices targetDevices) { diff --git a/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperationsTest.java b/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java similarity index 96% rename from src/test/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperationsTest.java rename to src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java index 3c529a0..2823691 100644 --- a/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultStorageDeviceOperationsTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java @@ -25,13 +25,13 @@ import static org.assertj.core.api.Assertions.assertThat; -class DefaultStorageDeviceOperationsTest { +class DefaultDeviceOperationsTest { PlayerStateRepository playerStateRepository = new InMemoryPlayerStateRepository(); PlayerState2CurrentPlayerStateConverter playerStateConverter = new Converters().playerState2CurrentPlayerStateConverter(); - DefaultStorageDeviceOperations operations = new DefaultStorageDeviceOperations( + DefaultDeviceOperations operations = new DefaultDeviceOperations( new PlayerStateService(playerStateRepository, playerStateConverter, new DefaultPlayerStateEntityFactory(new DeviceEntity.Factory(), new TrackItemEntity.Factory()) ), diff --git a/src/test/java/testing/stub/NullDeviceOperations.java b/src/test/java/testing/stub/NullDeviceOperations.java index df9ddac..edacaf0 100644 --- a/src/test/java/testing/stub/NullDeviceOperations.java +++ b/src/test/java/testing/stub/NullDeviceOperations.java @@ -20,12 +20,6 @@ public Mono addDevice(User user, Device device) { return Mono.empty(); } - @NotNull - @Override - public Mono containsById(User user, String deviceId) { - return Mono.empty(); - } - @NotNull @Override public Mono transferPlayback(User user, SwitchDeviceCommandArgs args, TargetDeactivationDevices deactivationDevices, TargetDevices targetDevices) { From d39fd533f09b10280d70f90cd630728dc4cb8d5b Mon Sep 17 00:00:00 2001 From: odeyalo Date: Tue, 27 Aug 2024 15:41:55 +0300 Subject: [PATCH 07/11] Refactoring of DefaultDeviceOperations, renamed addDevice to connectDevice, refactored tests --- .../connect/controller/DevicesController.java | 2 +- .../odeyalo/sonata/connect/model/Devices.java | 20 +- .../player/DefaultDeviceOperations.java | 4 +- .../service/player/DeviceOperations.java | 2 +- ...entPublisherDeviceOperationsDecorator.java | 4 +- .../player/DefaultDeviceOperationsTest.java | 203 ++++++++++++------ .../testing/stub/NullDeviceOperations.java | 40 ---- 7 files changed, 162 insertions(+), 113 deletions(-) delete mode 100644 src/test/java/testing/stub/NullDeviceOperations.java diff --git a/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java b/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java index e0c2188..db01167 100644 --- a/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java +++ b/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java @@ -39,7 +39,7 @@ public Mono> getAvailableDevices(@NotNull final User user) { public Mono> addDevice(@NotNull final User user, @NotNull @ConnectionTarget final Device device) { - return deviceOperations.addDevice(user, device) + return deviceOperations.connectDevice(user, device) .subscribeOn(Schedulers.boundedElastic()) .thenReturn(HttpStatus.default204Response()); } diff --git a/src/main/java/com/odeyalo/sonata/connect/model/Devices.java b/src/main/java/com/odeyalo/sonata/connect/model/Devices.java index 5b9da21..538cbd9 100644 --- a/src/main/java/com/odeyalo/sonata/connect/model/Devices.java +++ b/src/main/java/com/odeyalo/sonata/connect/model/Devices.java @@ -34,6 +34,11 @@ public static Devices empty() { return builder().build(); } + @NotNull + public static Devices of(final Device... devices) { + return fromCollection(Arrays.asList(devices)); + } + public boolean isEmpty() { return devices.isEmpty(); } @@ -129,6 +134,7 @@ public Devices deactivateDevice(@NotNull final Device deviceToDeactivate) { /** * Change the volume for ACTIVE device + * * @param volume - volume to set for active deivce * @return - updated {@link Devices} * @throws NoActiveDeviceException - if there is no active device present @@ -150,13 +156,6 @@ private Device findDeviceToActivate(@NotNull final TargetDevice searchTarget) { .orElseThrow(() -> DeviceNotFoundException.withCustomMessage(String.format("Device with ID: %s not found!", searchTarget.getId()))); } - @Nullable - private Device findCurrentlyActiveDevice() { - return getActiveDevices().stream() - .findFirst() - .orElse(null); - } - @NotNull private Devices removeDevice(@NotNull final String deviceId) { return getDevices().stream() @@ -164,6 +163,13 @@ private Devices removeDevice(@NotNull final String deviceId) { .collect(CollectorImpl.instance()); } + @Nullable + private Device findCurrentlyActiveDevice() { + return getActiveDevices().stream() + .findFirst() + .orElse(null); + } + @NotNull private Devices addDevice(@NotNull final Device device) { return Devices.builder() diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java index 8d536b1..470ed3e 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java @@ -26,8 +26,8 @@ public DefaultDeviceOperations(PlayerStateService playerStateService, @NotNull @Override - public Mono addDevice(@NotNull final User user, - @NotNull final Device device) { + public Mono connectDevice(@NotNull final User user, + @NotNull final Device device) { return playerStateService.loadPlayerState(user) .map(playerState -> playerState.connectDevice(device)) .flatMap(playerStateService::save); diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java index c246a55..cbe89b2 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java @@ -14,7 +14,7 @@ public interface DeviceOperations { @NotNull - Mono addDevice(User user, Device device); + Mono connectDevice(User user, Device device); /** * Transfer the playback to given devices diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java index 5b9a36c..c74db59 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java @@ -25,8 +25,8 @@ public EventPublisherDeviceOperationsDecorator(DeviceOperations delegate, Player @NotNull @Override - public Mono addDevice(User user, Device device) { - return delegate.addDevice(user, device) + public Mono connectDevice(User user, Device device) { + return delegate.connectDevice(user, device) .flatMap(state -> synchronizationManager.publishUpdatedState(user, DeviceConnectedPlayerEvent.builder() .playerState(state) diff --git a/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java b/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java index 2823691..07692f4 100644 --- a/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperationsTest.java @@ -2,94 +2,177 @@ import com.odeyalo.sonata.connect.config.Converters; import com.odeyalo.sonata.connect.entity.DeviceEntity; -import com.odeyalo.sonata.connect.entity.DevicesEntity; -import com.odeyalo.sonata.connect.entity.PlayerStateEntity; import com.odeyalo.sonata.connect.entity.TrackItemEntity; import com.odeyalo.sonata.connect.entity.factory.DefaultPlayerStateEntityFactory; -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.model.Device; -import com.odeyalo.sonata.connect.model.Devices; -import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.model.*; import com.odeyalo.sonata.connect.repository.InMemoryPlayerStateRepository; import com.odeyalo.sonata.connect.repository.PlayerStateRepository; import com.odeyalo.sonata.connect.service.player.handler.TransferPlaybackCommandHandlerDelegate; import com.odeyalo.sonata.connect.service.support.mapper.PlayerState2CurrentPlayerStateConverter; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import reactor.test.StepVerifier; -import testing.faker.DeviceEntityFaker; import testing.faker.DeviceFaker; -import testing.faker.PlayerStateFaker; +import java.util.Objects; + +import static com.odeyalo.sonata.connect.service.player.DefaultDeviceOperationsTest.TestableBuilder.testableBuilder; import static org.assertj.core.api.Assertions.assertThat; class DefaultDeviceOperationsTest { - PlayerStateRepository playerStateRepository = new InMemoryPlayerStateRepository(); - - PlayerState2CurrentPlayerStateConverter playerStateConverter = new Converters().playerState2CurrentPlayerStateConverter(); - - DefaultDeviceOperations operations = new DefaultDeviceOperations( - new PlayerStateService(playerStateRepository, playerStateConverter, - new DefaultPlayerStateEntityFactory(new DeviceEntity.Factory(), new TrackItemEntity.Factory()) - ), - Mockito.mock(TransferPlaybackCommandHandlerDelegate.class) - ); - final User USER = User.of("miku"); - final DeviceEntity ACTIVE_DEVICE = DeviceEntityFaker.createActiveDevice().get(); - final DeviceEntity INACTIVE_DEVICE = DeviceEntityFaker.createInactiveDevice().get(); - @BeforeEach - void prepare() { - final PlayerStateEntity entity = PlayerStateFaker.forUser(USER) - .devicesEntity(DevicesEntity.just(ACTIVE_DEVICE, INACTIVE_DEVICE)) - .get(); - - playerStateRepository.save(entity).block(); + @Test + void shouldConnectDeviceAndMakeItIdleIfOtherDevicesAreAlreadyConnected() { + final DefaultDeviceOperations testable = testableBuilder() + .createEmptyPlayerStateFor(USER) + .connectDevices( + DeviceFaker.create().get(), + DeviceFaker.create().get() + ) + .build(); + + final Device connectionTarget = Device.builder() + .deviceId("123") + .deviceName("miku") + .deviceType(DeviceType.COMPUTER) + .status(DeviceSpec.DeviceStatus.IDLE) + .volume(Volume.from(20)) + .build(); + + final CurrentPlayerState playerState = testable.connectDevice(USER, connectionTarget).block(); + + assertThat(playerState).isNotNull(); + + assertThat(playerState.getDevices()) + .hasSize(3) + .filteredOn(device -> Objects.equals(device.getDeviceName(), "miku")) + .first() + .satisfies(device -> { + assertThat(device.getId()).isEqualTo("123"); + assertThat(device.getType()).isEqualTo(DeviceType.COMPUTER); + assertThat(device.getVolume()).isEqualTo(Volume.from(20)); + assertThat(device.isIdle()).isTrue(); + }); } @Test - void shouldConnectDevice() { - final Device device = DeviceFaker.create().get(); - - operations.addDevice(USER, device) - .as(StepVerifier::create) - .assertNext(state -> assertThat(state.getDevices()).contains(device)) - .verifyComplete(); + void shouldConnectDeviceAndMakeItActiveIfNoOtherDevicesIsConnected() { + final DefaultDeviceOperations testable = testableBuilder() + .createEmptyPlayerStateFor(USER) + .build(); + + final Device connectionTarget = Device.builder() + .deviceId("123") + .deviceName("miku") + .deviceType(DeviceType.COMPUTER) + .status(DeviceSpec.DeviceStatus.IDLE) + .volume(Volume.from(20)) + .build(); + + final CurrentPlayerState playerState = testable.connectDevice(USER, connectionTarget).block(); + + assertThat(playerState).isNotNull(); + + assertThat(playerState.getDevices()) + .hasSize(1) + .first() + .satisfies(device -> { + assertThat(device.getId()).isEqualTo("123"); + assertThat(device.getName()).isEqualTo("miku"); + assertThat(device.getType()).isEqualTo(DeviceType.COMPUTER); + assertThat(device.getVolume()).isEqualTo(Volume.from(20)); + assertThat(device.isActive()).isTrue(); + }); } @Test - void shouldRemoveExistingDevice() { - final String disconnectTargetId = INACTIVE_DEVICE.getId(); - - operations.disconnectDevice(USER, DisconnectDeviceArgs.withDeviceId(disconnectTargetId)) - .map(CurrentPlayerState::getDevices) - .as(StepVerifier::create) - .assertNext(devices -> assertThat(devices) - .extracting(Device::getDeviceId) - .doesNotContain(disconnectTargetId) + void shouldDisconnectDeviceByItsIdIfDeviceIsConnected() { + final DefaultDeviceOperations testable = testableBuilder() + .createEmptyPlayerStateFor(USER) + .connectDevices( + DeviceFaker.create().get().withActiveStatus(), + DeviceFaker.create().get().withDeviceId("inactive").withIdleStatus() ) - .verifyComplete(); + .build(); + + final CurrentPlayerState playerState = testable.disconnectDevice(USER, DisconnectDeviceArgs.withDeviceId("inactive")).block(); + + assertThat(playerState).isNotNull(); + + assertThat(playerState.getDevices()) + .extracting(Device::getDeviceId) + .doesNotContain("inactive"); } @Test void shouldNotChangeStateIfDeviceNotExist() { - String disconnectTargetId = "not_existing"; - Devices connectedDevices = operations.getConnectedDevices(USER).block(); - - //noinspection DataFlowIssue - operations.disconnectDevice(USER, DisconnectDeviceArgs.withDeviceId(disconnectTargetId)) - .map(CurrentPlayerState::getDevices) - .as(StepVerifier::create) - .expectNext(connectedDevices) - .verifyComplete(); + final Devices connectedDevices = Devices.of( + DeviceFaker.create().get().withActiveStatus(), + DeviceFaker.create().get().withIdleStatus() + ); + + final DefaultDeviceOperations testable = testableBuilder() + .createEmptyPlayerStateFor(USER) + .connectDevices(connectedDevices) + .build(); + + final CurrentPlayerState playerState = testable.disconnectDevice(USER, DisconnectDeviceArgs.withDeviceId("not_existing")).block(); + + assertThat(playerState).isNotNull(); + assertThat(playerState.getDevices()).containsAll(connectedDevices); } - @AfterEach - void tearDown() { - playerStateRepository.clear().block(); + + static final class TestableBuilder { + private final PlayerStateRepository playerStateRepository = new InMemoryPlayerStateRepository(); + + private final PlayerState2CurrentPlayerStateConverter playerStateConverter = new Converters().playerState2CurrentPlayerStateConverter(); + private final TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate = Mockito.mock(TransferPlaybackCommandHandlerDelegate.class); + + @Nullable + private CurrentPlayerState currentPlayerState; + + public static TestableBuilder testableBuilder() { + return new TestableBuilder(); + } + + public TestableBuilder createEmptyPlayerStateFor(final User user) { + currentPlayerState = CurrentPlayerState.emptyFor(user); + return this; + } + + public TestableBuilder connectDevices(final Device... devices) { + return connectDevices(Devices.of(devices)); + } + + public TestableBuilder connectDevices(final Devices devices) { + if ( currentPlayerState == null ) { + throw new IllegalStateException("Devices can't be connected if no player state exist. Use createEmptyPlayerStateFor(User) method and then call it"); + } + + for (final Device device : devices) { + currentPlayerState = currentPlayerState.connectDevice(device); + } + + return this; + } + + @NotNull + public DefaultDeviceOperations build() { + final PlayerStateService playerStateService = new PlayerStateService( + playerStateRepository, playerStateConverter, new DefaultPlayerStateEntityFactory(new DeviceEntity.Factory(), new TrackItemEntity.Factory()) + ); + + if ( currentPlayerState != null ) { + playerStateService.save(currentPlayerState).block(); + } + + return new DefaultDeviceOperations( + playerStateService, transferPlaybackCommandHandlerDelegate + ); + } } } \ No newline at end of file diff --git a/src/test/java/testing/stub/NullDeviceOperations.java b/src/test/java/testing/stub/NullDeviceOperations.java deleted file mode 100644 index edacaf0..0000000 --- a/src/test/java/testing/stub/NullDeviceOperations.java +++ /dev/null @@ -1,40 +0,0 @@ -package testing.stub; - -import com.odeyalo.sonata.connect.model.CurrentPlayerState; -import com.odeyalo.sonata.connect.model.Device; -import com.odeyalo.sonata.connect.model.Devices; -import com.odeyalo.sonata.connect.model.User; -import com.odeyalo.sonata.connect.service.player.DeviceOperations; -import com.odeyalo.sonata.connect.service.player.DisconnectDeviceArgs; -import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; -import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; -import org.jetbrains.annotations.NotNull; -import reactor.core.publisher.Mono; - -public class NullDeviceOperations implements DeviceOperations { - - @NotNull - @Override - public Mono addDevice(User user, Device device) { - return Mono.empty(); - } - - @NotNull - @Override - public Mono transferPlayback(User user, SwitchDeviceCommandArgs args, TargetDeactivationDevices deactivationDevices, TargetDevices targetDevices) { - return Mono.empty(); - } - - @NotNull - @Override - public Mono disconnectDevice(User user, DisconnectDeviceArgs args) { - return Mono.empty(); - } - - @NotNull - @Override - public Mono getConnectedDevices(User user) { - return Mono.empty(); - } -} From ea334052667480890c802d3947c4cf2c40cc5e0a Mon Sep 17 00:00:00 2001 From: odeyalo Date: Tue, 27 Aug 2024 15:43:23 +0300 Subject: [PATCH 08/11] Made DefaultDeviceOperations final --- .../sonata/connect/service/player/DefaultDeviceOperations.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java index 470ed3e..e2cba4b 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java @@ -12,7 +12,7 @@ import reactor.core.publisher.Mono; @Component -public class DefaultDeviceOperations implements DeviceOperations { +public final class DefaultDeviceOperations implements DeviceOperations { private final PlayerStateService playerStateService; private final TransferPlaybackCommandHandlerDelegate transferPlaybackCommandHandlerDelegate; From d0a1441a79137a84f09ccfdcd440ae8c8d7ea878 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Tue, 27 Aug 2024 15:52:27 +0300 Subject: [PATCH 09/11] Moved TargetDevices.java to other package --- .../odeyalo/sonata/connect/controller/DevicesController.java | 2 +- .../sonata/connect/service/{player/sync => }/TargetDevices.java | 2 +- .../sonata/connect/service/player/DefaultDeviceOperations.java | 2 +- .../odeyalo/sonata/connect/service/player/DeviceOperations.java | 2 +- .../service/player/EventPublisherDeviceOperationsDecorator.java | 2 +- .../SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java | 2 +- .../player/handler/TransferPlaybackCommandHandlerDelegate.java | 2 +- .../support/web/resolver/TransferPlaybackDevicesResolver.java | 2 +- ...gleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) rename src/main/java/com/odeyalo/sonata/connect/service/{player/sync => }/TargetDevices.java (97%) diff --git a/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java b/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java index db01167..c3cb859 100644 --- a/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java +++ b/src/main/java/com/odeyalo/sonata/connect/controller/DevicesController.java @@ -2,11 +2,11 @@ import com.odeyalo.sonata.connect.model.Device; import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.DeviceOperations; import com.odeyalo.sonata.connect.service.player.DisconnectDeviceArgs; import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import com.odeyalo.sonata.connect.service.support.mapper.dto.AvailableDevicesResponseDtoConverter; import com.odeyalo.sonata.connect.support.web.HttpStatus; import com.odeyalo.sonata.connect.support.web.annotation.ConnectionTarget; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/sync/TargetDevices.java b/src/main/java/com/odeyalo/sonata/connect/service/TargetDevices.java similarity index 97% rename from src/main/java/com/odeyalo/sonata/connect/service/player/sync/TargetDevices.java rename to src/main/java/com/odeyalo/sonata/connect/service/TargetDevices.java index 56aeb08..2771253 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/sync/TargetDevices.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/TargetDevices.java @@ -1,4 +1,4 @@ -package com.odeyalo.sonata.connect.service.player.sync; +package com.odeyalo.sonata.connect.service; import com.odeyalo.sonata.connect.service.player.TargetDevice; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java index e2cba4b..c190e83 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DefaultDeviceOperations.java @@ -4,8 +4,8 @@ import com.odeyalo.sonata.connect.model.Device; import com.odeyalo.sonata.connect.model.Devices; import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.handler.TransferPlaybackCommandHandlerDelegate; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java index cbe89b2..cb16a26 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/DeviceOperations.java @@ -4,7 +4,7 @@ import com.odeyalo.sonata.connect.model.Device; import com.odeyalo.sonata.connect.model.Devices; import com.odeyalo.sonata.connect.model.User; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; +import com.odeyalo.sonata.connect.service.TargetDevices; import org.jetbrains.annotations.NotNull; import reactor.core.publisher.Mono; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java index c74db59..18e0503 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/EventPublisherDeviceOperationsDecorator.java @@ -4,8 +4,8 @@ import com.odeyalo.sonata.connect.model.Device; import com.odeyalo.sonata.connect.model.Devices; import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.sync.PlayerSynchronizationManager; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import com.odeyalo.sonata.connect.service.player.sync.event.DeviceConnectedPlayerEvent; import com.odeyalo.sonata.connect.service.player.sync.event.DeviceDisconnectedPlayerEvent; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java index 8e2f02d..949367a 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegate.java @@ -5,11 +5,11 @@ import com.odeyalo.sonata.connect.exception.TargetDeviceRequiredException; import com.odeyalo.sonata.connect.model.CurrentPlayerState; import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.PlayerStateService; import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; import com.odeyalo.sonata.connect.service.player.TargetDevice; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java index 92289b2..42c2dc8 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java @@ -2,10 +2,10 @@ import com.odeyalo.sonata.connect.model.CurrentPlayerState; import com.odeyalo.sonata.connect.model.User; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.BasicPlayerOperations; import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import org.jetbrains.annotations.NotNull; import reactor.core.publisher.Mono; diff --git a/src/main/java/com/odeyalo/sonata/connect/support/web/resolver/TransferPlaybackDevicesResolver.java b/src/main/java/com/odeyalo/sonata/connect/support/web/resolver/TransferPlaybackDevicesResolver.java index 86f6cbd..2a05432 100644 --- a/src/main/java/com/odeyalo/sonata/connect/support/web/resolver/TransferPlaybackDevicesResolver.java +++ b/src/main/java/com/odeyalo/sonata/connect/support/web/resolver/TransferPlaybackDevicesResolver.java @@ -1,7 +1,7 @@ package com.odeyalo.sonata.connect.support.web.resolver; import com.odeyalo.sonata.connect.dto.DeviceSwitchRequest; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.support.web.annotation.TransferPlaybackTargets; import jakarta.validation.Valid; import org.jetbrains.annotations.NotNull; diff --git a/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java b/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java index d84ebb8..10e5ba7 100644 --- a/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/service/player/handler/SingleDeviceOnlyTransferPlaybackCommandHandlerDelegateTest.java @@ -14,11 +14,11 @@ import com.odeyalo.sonata.connect.model.User; import com.odeyalo.sonata.connect.repository.InMemoryPlayerStateRepository; import com.odeyalo.sonata.connect.repository.PlayerStateRepository; +import com.odeyalo.sonata.connect.service.TargetDevices; import com.odeyalo.sonata.connect.service.player.PlayerStateService; import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; import com.odeyalo.sonata.connect.service.player.TargetDevice; -import com.odeyalo.sonata.connect.service.player.sync.TargetDevices; import com.odeyalo.sonata.connect.service.support.mapper.PlayerState2CurrentPlayerStateConverter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; From 44946c5edada60fa743e87f191a1818974a4b511 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Tue, 27 Aug 2024 15:53:22 +0300 Subject: [PATCH 10/11] Fix docs in TransferPlaybackCommandHandlerDelegate --- .../handler/TransferPlaybackCommandHandlerDelegate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java index 42c2dc8..104931c 100644 --- a/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java +++ b/src/main/java/com/odeyalo/sonata/connect/service/player/handler/TransferPlaybackCommandHandlerDelegate.java @@ -3,7 +3,7 @@ import com.odeyalo.sonata.connect.model.CurrentPlayerState; import com.odeyalo.sonata.connect.model.User; import com.odeyalo.sonata.connect.service.TargetDevices; -import com.odeyalo.sonata.connect.service.player.BasicPlayerOperations; +import com.odeyalo.sonata.connect.service.player.DeviceOperations; import com.odeyalo.sonata.connect.service.player.SwitchDeviceCommandArgs; import com.odeyalo.sonata.connect.service.player.TargetDeactivationDevices; import org.jetbrains.annotations.NotNull; @@ -21,7 +21,7 @@ public interface TransferPlaybackCommandHandlerDelegate { * @param targetDevices - devices to transfer playback. * @return - Mono with updated player state * - * @see BasicPlayerOperations#getDeviceOperations()#transferPlayback(User, SwitchDeviceCommandArgs, TargetDeactivationDevices, TargetDevices) + * @see DeviceOperations()#transferPlayback(User, SwitchDeviceCommandArgs, TargetDeactivationDevices, TargetDevices) */ @NotNull Mono transferPlayback(User user, SwitchDeviceCommandArgs args, TargetDeactivationDevices deactivationDevices, TargetDevices targetDevices); From 22c23abd1121bcb58115045f9f27db85557eef66 Mon Sep 17 00:00:00 2001 From: odeyalo Date: Tue, 27 Aug 2024 16:20:52 +0300 Subject: [PATCH 11/11] Fix failing test when wrong error description when device not found was returned as result to client --- .../exception/GlobalExceptionHandlerController.java | 7 +++---- .../connect/controller/SwitchDevicesEndpointTest.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java b/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java index b5c793e..38d4334 100644 --- a/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java +++ b/src/main/java/com/odeyalo/sonata/connect/exception/GlobalExceptionHandlerController.java @@ -11,13 +11,12 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.support.WebExchangeBindException; -import static org.springframework.http.ResponseEntity.*; +import static org.springframework.http.ResponseEntity.badRequest; +import static org.springframework.http.ResponseEntity.unprocessableEntity; @RestControllerAdvice public class GlobalExceptionHandlerController { - public static final String NO_ACTIVE_DEVICE_ERROR_DESCRIPTION = "Player command failed: No active device found"; - @ExceptionHandler(WebExchangeBindException.class) public ResponseEntity handleWebExchangeBindException(WebExchangeBindException ex) { ExceptionMessages messages = ExceptionMessages.empty(); @@ -37,7 +36,7 @@ public ResponseEntity handleIllegalStateException(IllegalState @ExceptionHandler(NoActiveDeviceException.class) public ResponseEntity handleNoActiveDeviceException(NoActiveDeviceException ex) { - return badRequest().body(ReasonCodeAwareExceptionMessage.of(ex.getReasonCode(), NO_ACTIVE_DEVICE_ERROR_DESCRIPTION)); + return badRequest().body(ReasonCodeAwareExceptionMessage.of(ex.getReasonCode(), "Player command failed: No active device found")); } diff --git a/src/test/java/com/odeyalo/sonata/connect/controller/SwitchDevicesEndpointTest.java b/src/test/java/com/odeyalo/sonata/connect/controller/SwitchDevicesEndpointTest.java index c4df9b1..749a879 100644 --- a/src/test/java/com/odeyalo/sonata/connect/controller/SwitchDevicesEndpointTest.java +++ b/src/test/java/com/odeyalo/sonata/connect/controller/SwitchDevicesEndpointTest.java @@ -112,7 +112,7 @@ void shouldReturnExceptionMessage() { ExceptionMessage message = responseSpec.expectBody(ExceptionMessage.class).returnResult().getResponseBody(); - ExceptionMessageAssert.forMessage(message).isDescriptionEqualTo("Device with provided ID was not found!"); + ExceptionMessageAssert.forMessage(message).isDescriptionEqualTo("Device with ID: not_existing not found!"); } @NotNull