Merge branch 'master' of https://github.com/airsonic/airsonic into transifex_update
commit
b8b5dc59a0
@ -0,0 +1,12 @@ |
|||||||
|
package org.airsonic.player.dao; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.PlayQueue; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
@Component |
||||||
|
public class PlayerDaoPlayQueueFactory { |
||||||
|
|
||||||
|
public PlayQueue createPlayQueue() { |
||||||
|
return new PlayQueue(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package org.airsonic.player.service.jukebox; |
||||||
|
|
||||||
|
import org.airsonic.player.service.JukeboxLegacySubsonicService; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
|
||||||
|
@Component |
||||||
|
public class AudioPlayerFactory { |
||||||
|
|
||||||
|
public AudioPlayer createAudioPlayer(InputStream in, JukeboxLegacySubsonicService jukeboxLegacySubsonicService) throws Exception { |
||||||
|
return new AudioPlayer(in, jukeboxLegacySubsonicService); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package org.airsonic.player.service.jukebox; |
||||||
|
|
||||||
|
import com.github.biconou.AudioPlayer.JavaPlayer; |
||||||
|
import com.github.biconou.AudioPlayer.api.Player; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
@Component |
||||||
|
public class JavaPlayerFactory { |
||||||
|
|
||||||
|
public Player createJavaPlayer() { |
||||||
|
return new JavaPlayer(); |
||||||
|
} |
||||||
|
|
||||||
|
public Player createJavaPlayer(String mixerName) { |
||||||
|
return new JavaPlayer(mixerName); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
package org.airsonic.player.api; |
||||||
|
|
||||||
|
import org.airsonic.player.TestCaseUtils; |
||||||
|
import org.airsonic.player.util.HomeRule; |
||||||
|
import org.junit.BeforeClass; |
||||||
|
import org.junit.ClassRule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
|
||||||
|
@RunWith(SpringRunner.class) |
||||||
|
@SpringBootTest |
||||||
|
@AutoConfigureMockMvc |
||||||
|
public class AirsonicRestApiIntTest { |
||||||
|
|
||||||
|
public static final String CLIENT_NAME = "airsonic"; |
||||||
|
public static final String AIRSONIC_USER = "admin"; |
||||||
|
public static final String AIRSONIC_PASSWORD = "admin"; |
||||||
|
public static final String EXPECTED_FORMAT = "json"; |
||||||
|
|
||||||
|
private static String AIRSONIC_API_VERSION; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MockMvc mvc; |
||||||
|
|
||||||
|
@ClassRule |
||||||
|
public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
|
||||||
|
|
||||||
|
@BeforeClass |
||||||
|
public static void setupClass() { |
||||||
|
AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void pingTest() throws Exception { |
||||||
|
mvc.perform(get("/rest/ping") |
||||||
|
.param("v", AIRSONIC_API_VERSION) |
||||||
|
.param("c", CLIENT_NAME) |
||||||
|
.param("u", AIRSONIC_USER) |
||||||
|
.param("p", AIRSONIC_PASSWORD) |
||||||
|
.param("f", EXPECTED_FORMAT) |
||||||
|
.contentType(MediaType.APPLICATION_JSON)) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.status").value("ok")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.version").value(AIRSONIC_API_VERSION)) |
||||||
|
.andDo(print()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,279 @@ |
|||||||
|
package org.airsonic.player.api.jukebox; |
||||||
|
|
||||||
|
import org.airsonic.player.TestCaseUtils; |
||||||
|
import org.airsonic.player.dao.*; |
||||||
|
import org.airsonic.player.domain.*; |
||||||
|
import org.airsonic.player.service.MediaScannerService; |
||||||
|
import org.airsonic.player.service.PlayerService; |
||||||
|
import org.airsonic.player.service.SettingsService; |
||||||
|
import org.airsonic.player.util.HomeRule; |
||||||
|
import org.junit.*; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
||||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||||
|
import org.springframework.boot.test.context.TestConfiguration; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
import org.springframework.test.annotation.DirtiesContext; |
||||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||||
|
import org.springframework.test.web.servlet.ResultActions; |
||||||
|
import org.springframework.test.web.servlet.ResultMatcher; |
||||||
|
|
||||||
|
import java.text.SimpleDateFormat; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.TimeZone; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.mockito.Mockito.*; |
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||||
|
|
||||||
|
@RunWith(SpringRunner.class) |
||||||
|
@SpringBootTest |
||||||
|
@AutoConfigureMockMvc |
||||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) |
||||||
|
public abstract class AbstractAirsonicRestApiJukeboxIntTest { |
||||||
|
|
||||||
|
@ClassRule |
||||||
|
public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
|
||||||
|
|
||||||
|
@TestConfiguration |
||||||
|
static class Config { |
||||||
|
private static class SpiedPlayerDaoPlayQueueFactory extends PlayerDaoPlayQueueFactory { |
||||||
|
@Override |
||||||
|
public PlayQueue createPlayQueue() { |
||||||
|
return spy(super.createPlayQueue()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public PlayerDaoPlayQueueFactory playerDaoPlayQueueFactory() { |
||||||
|
return new SpiedPlayerDaoPlayQueueFactory(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected static final String CLIENT_NAME = "airsonic"; |
||||||
|
protected static final String JUKEBOX_PLAYER_NAME = CLIENT_NAME + "-jukebox"; |
||||||
|
private static final String EXPECTED_FORMAT = "json"; |
||||||
|
private static String AIRSONIC_API_VERSION; |
||||||
|
|
||||||
|
private static boolean dataBasePopulated; |
||||||
|
private static DaoHelper staticDaoHelper; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
protected PlayerService playerService; |
||||||
|
@Autowired |
||||||
|
private MockMvc mvc; |
||||||
|
@Autowired |
||||||
|
private MusicFolderDao musicFolderDao; |
||||||
|
@Autowired |
||||||
|
private SettingsService settingsService; |
||||||
|
@Autowired |
||||||
|
private MediaScannerService mediaScannerService; |
||||||
|
@Autowired |
||||||
|
private PlayerDao playerDao; |
||||||
|
@Autowired |
||||||
|
private MediaFileDao mediaFileDao; |
||||||
|
@Autowired |
||||||
|
private DaoHelper daoHelper; |
||||||
|
@Autowired |
||||||
|
private AlbumDao albumDao; |
||||||
|
@Autowired |
||||||
|
private ArtistDao artistDao; |
||||||
|
|
||||||
|
private Player testJukeboxPlayer; |
||||||
|
|
||||||
|
@BeforeClass |
||||||
|
public static void setupClass() { |
||||||
|
AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion(); |
||||||
|
dataBasePopulated = false; |
||||||
|
} |
||||||
|
|
||||||
|
@AfterClass |
||||||
|
public static void cleanDataBase() { |
||||||
|
staticDaoHelper.getJdbcTemplate().execute("DROP SCHEMA PUBLIC CASCADE"); |
||||||
|
staticDaoHelper = null; |
||||||
|
dataBasePopulated = false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Populate test datas in the database only once. |
||||||
|
* |
||||||
|
* <ul> |
||||||
|
* <li>Creates 2 music folder</li> |
||||||
|
* <li>Scans the music folders</li> |
||||||
|
* <li>Creates a test jukebox player</li> |
||||||
|
* </ul> |
||||||
|
*/ |
||||||
|
private void populateDatabase() { |
||||||
|
if (!dataBasePopulated) { |
||||||
|
staticDaoHelper = daoHelper; |
||||||
|
|
||||||
|
assertThat(musicFolderDao.getAllMusicFolders().size()).isEqualTo(1); |
||||||
|
MusicFolderTestData.getTestMusicFolders().forEach(musicFolderDao::createMusicFolder); |
||||||
|
settingsService.clearMusicFolderCache(); |
||||||
|
|
||||||
|
TestCaseUtils.execScan(mediaScannerService); |
||||||
|
|
||||||
|
assertThat(playerDao.getAllPlayers().size()).isEqualTo(0); |
||||||
|
createTestPlayer(); |
||||||
|
assertThat(playerDao.getAllPlayers().size()).isEqualTo(1); |
||||||
|
|
||||||
|
dataBasePopulated = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup() throws Exception { |
||||||
|
populateDatabase(); |
||||||
|
|
||||||
|
testJukeboxPlayer = findTestJukeboxPlayer(); |
||||||
|
assertThat(testJukeboxPlayer).isNotNull(); |
||||||
|
reset(testJukeboxPlayer.getPlayQueue()); |
||||||
|
testJukeboxPlayer.getPlayQueue().clear(); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(0); |
||||||
|
testJukeboxPlayer.getPlayQueue().addFiles(true, |
||||||
|
mediaFileDao.getSongsForAlbum("_DIR_ Ravel", "Complete Piano Works")); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(2); |
||||||
|
} |
||||||
|
|
||||||
|
protected abstract void createTestPlayer(); |
||||||
|
|
||||||
|
private Player findTestJukeboxPlayer() { |
||||||
|
return playerDao.getAllPlayers().stream().filter(player -> player.getName().equals(JUKEBOX_PLAYER_NAME)) |
||||||
|
.findFirst().orElseThrow(() -> new RuntimeException("No player found in database")); |
||||||
|
} |
||||||
|
|
||||||
|
private String convertDateToString(Date date) { |
||||||
|
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.000'Z'"); |
||||||
|
formatter.setTimeZone(TimeZone.getTimeZone("UTC")); |
||||||
|
return formatter.format(date); |
||||||
|
} |
||||||
|
|
||||||
|
private ResultMatcher playListItem1isCorrect() { |
||||||
|
MediaFile mediaFile = testJukeboxPlayer.getPlayQueue().getFile(0); |
||||||
|
MediaFile parent = mediaFileDao.getMediaFile(mediaFile.getParentPath()); |
||||||
|
Album album = albumDao.getAlbum(mediaFile.getArtist(), mediaFile.getAlbumName()); |
||||||
|
Artist artist = artistDao.getArtist(mediaFile.getArtist()); |
||||||
|
assertThat(album).isNotNull(); |
||||||
|
return result -> { |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].id").value(mediaFile.getId()).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].parent").value(parent.getId()).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isDir").value(false).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].title").value("01 - Gaspard de la Nuit - i. Ondine").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].album").value("Complete Piano Works").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artist").value("_DIR_ Ravel").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].coverArt").value(parent.getId()).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].size").value(45138).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].contentType").value("audio/mpeg").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].suffix").value("mp3").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].duration").value(2).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].bitRate").value(128).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].path").value("_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/01 - Gaspard de la Nuit - i. Ondine.mp3").match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isVideo").value(false).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].playCount").isNumber().match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].created").value(convertDateToString(mediaFile.getCreated())).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].albumId").value(album.getId()).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artistId").value(artist.getId()).match(result); |
||||||
|
jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].type").value("music").match(result); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(username = "admin") |
||||||
|
public void jukeboxStartActionTest() throws Exception { |
||||||
|
// Given
|
||||||
|
|
||||||
|
// When and Then
|
||||||
|
performStartAction(); |
||||||
|
performStatusAction("true"); |
||||||
|
performGetAction() |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.currentIndex").value("0")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.playing").value("true")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.gain").value("0.75")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.position").value("0")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry").isArray()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry.length()").value(2)) |
||||||
|
.andExpect(playListItem1isCorrect()) |
||||||
|
.andDo(print()); |
||||||
|
|
||||||
|
verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.PLAYING); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(username = "admin") |
||||||
|
public void jukeboxStopActionTest() throws Exception { |
||||||
|
// Given
|
||||||
|
|
||||||
|
// When and Then
|
||||||
|
performStartAction(); |
||||||
|
performStatusAction("true"); |
||||||
|
performStopAction(); |
||||||
|
performStatusAction("false"); |
||||||
|
|
||||||
|
verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING); |
||||||
|
verify(testJukeboxPlayer.getPlayQueue(), times(1)).setStatus(PlayQueue.Status.STOPPED); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0); |
||||||
|
assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.STOPPED); |
||||||
|
} |
||||||
|
|
||||||
|
private void performStatusAction(String expectedPlayingValue) throws Exception { |
||||||
|
mvc.perform(get("/rest/jukeboxControl.view") |
||||||
|
.param("v", AIRSONIC_API_VERSION) |
||||||
|
.param("c", CLIENT_NAME) |
||||||
|
.param("f", EXPECTED_FORMAT) |
||||||
|
.param("action", "status") |
||||||
|
.contentType(MediaType.APPLICATION_JSON)) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.status").value("ok")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value(expectedPlayingValue)) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0")); |
||||||
|
} |
||||||
|
|
||||||
|
private ResultActions performGetAction() throws Exception { |
||||||
|
return mvc.perform(get("/rest/jukeboxControl.view") |
||||||
|
.param("v", AIRSONIC_API_VERSION) |
||||||
|
.param("c", CLIENT_NAME) |
||||||
|
.param("f", EXPECTED_FORMAT) |
||||||
|
.param("action", "get") |
||||||
|
.contentType(MediaType.APPLICATION_JSON)) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.status").value("ok")); |
||||||
|
} |
||||||
|
|
||||||
|
private void performStopAction() throws Exception { |
||||||
|
mvc.perform(get("/rest/jukeboxControl.view") |
||||||
|
.param("v", AIRSONIC_API_VERSION) |
||||||
|
.param("c", CLIENT_NAME) |
||||||
|
.param("f", EXPECTED_FORMAT) |
||||||
|
.param("action", "stop") |
||||||
|
.contentType(MediaType.APPLICATION_JSON)) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.status").value("ok")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("false")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0")); |
||||||
|
} |
||||||
|
|
||||||
|
private void performStartAction() throws Exception { |
||||||
|
mvc.perform(get("/rest/jukeboxControl.view") |
||||||
|
.param("v", AIRSONIC_API_VERSION) |
||||||
|
.param("c", CLIENT_NAME) |
||||||
|
.param("f", EXPECTED_FORMAT) |
||||||
|
.param("action", "start") |
||||||
|
.contentType(MediaType.APPLICATION_JSON)) |
||||||
|
.andExpect(status().isOk()) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.status").value("ok")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("true")) |
||||||
|
.andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0")); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
package org.airsonic.player.api.jukebox; |
||||||
|
|
||||||
|
import com.github.biconou.AudioPlayer.JavaPlayer; |
||||||
|
import org.airsonic.player.domain.Player; |
||||||
|
import org.airsonic.player.domain.PlayerTechnology; |
||||||
|
import org.airsonic.player.service.jukebox.JavaPlayerFactory; |
||||||
|
import org.junit.Before; |
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
public class AirsonicRestApiJukeboxIntTest extends AbstractAirsonicRestApiJukeboxIntTest { |
||||||
|
|
||||||
|
@MockBean |
||||||
|
protected JavaPlayerFactory javaPlayerFactory; |
||||||
|
|
||||||
|
@Before |
||||||
|
@Override |
||||||
|
public void setup() throws Exception { |
||||||
|
super.setup(); |
||||||
|
JavaPlayer mockJavaPlayer = mock(JavaPlayer.class); |
||||||
|
when(mockJavaPlayer.getPlayingInfos()).thenReturn( () -> 0 ); |
||||||
|
when(mockJavaPlayer.getGain()).thenReturn(0.75f); |
||||||
|
when(javaPlayerFactory.createJavaPlayer()).thenReturn(mockJavaPlayer); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void createTestPlayer() { |
||||||
|
Player jukeBoxPlayer = new Player(); |
||||||
|
jukeBoxPlayer.setName(JUKEBOX_PLAYER_NAME); |
||||||
|
jukeBoxPlayer.setUsername("admin"); |
||||||
|
jukeBoxPlayer.setClientId(CLIENT_NAME + "-jukebox"); |
||||||
|
jukeBoxPlayer.setTechnology(PlayerTechnology.JAVA_JUKEBOX); |
||||||
|
playerService.createPlayer(jukeBoxPlayer); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package org.airsonic.player.api.jukebox; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.Player; |
||||||
|
import org.airsonic.player.domain.PlayerTechnology; |
||||||
|
import org.airsonic.player.service.TranscodingService; |
||||||
|
import org.airsonic.player.service.jukebox.AudioPlayer; |
||||||
|
import org.airsonic.player.service.jukebox.AudioPlayerFactory; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean; |
||||||
|
import org.springframework.security.test.context.support.WithMockUser; |
||||||
|
|
||||||
|
import static org.mockito.Matchers.any; |
||||||
|
import static org.mockito.Mockito.*; |
||||||
|
|
||||||
|
public class AirsonicRestApiJukeboxLegacyIntTest extends AirsonicRestApiJukeboxIntTest { |
||||||
|
|
||||||
|
@SpyBean |
||||||
|
private TranscodingService transcodingService; |
||||||
|
@MockBean |
||||||
|
protected AudioPlayerFactory audioPlayerFactory; |
||||||
|
|
||||||
|
private AudioPlayer mockAudioPlayer; |
||||||
|
|
||||||
|
@Before |
||||||
|
@Override |
||||||
|
public void setup() throws Exception { |
||||||
|
super.setup(); |
||||||
|
mockAudioPlayer = mock(AudioPlayer.class); |
||||||
|
when(audioPlayerFactory.createAudioPlayer(any(), any())).thenReturn(mockAudioPlayer); |
||||||
|
doReturn(null).when(transcodingService).getTranscodedInputStream(any()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected final void createTestPlayer() { |
||||||
|
Player jukeBoxPlayer = new Player(); |
||||||
|
jukeBoxPlayer.setName(JUKEBOX_PLAYER_NAME); |
||||||
|
jukeBoxPlayer.setUsername("admin"); |
||||||
|
jukeBoxPlayer.setClientId(CLIENT_NAME + "-jukebox"); |
||||||
|
jukeBoxPlayer.setTechnology(PlayerTechnology.JUKEBOX); |
||||||
|
playerService.createPlayer(jukeBoxPlayer); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(username = "admin") |
||||||
|
@Override |
||||||
|
public void jukeboxStartActionTest() throws Exception { |
||||||
|
super.jukeboxStartActionTest(); |
||||||
|
verify(mockAudioPlayer).play(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
@WithMockUser(username = "admin") |
||||||
|
@Override |
||||||
|
public void jukeboxStopActionTest() throws Exception { |
||||||
|
super.jukeboxStopActionTest(); |
||||||
|
verify(mockAudioPlayer).play(); |
||||||
|
verify(mockAudioPlayer).pause(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package org.airsonic.player.service; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.Transcoding; |
||||||
|
import org.airsonic.player.util.HomeRule; |
||||||
|
import org.junit.ClassRule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean; |
||||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||||
|
import static org.mockito.Mockito.*; |
||||||
|
|
||||||
|
@RunWith(SpringRunner.class) |
||||||
|
@SpringBootTest |
||||||
|
public class TranscodingServiceIntTest { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private TranscodingService transcodingService; |
||||||
|
@SpyBean |
||||||
|
private PlayerService playerService; |
||||||
|
|
||||||
|
@ClassRule |
||||||
|
public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void createTranscodingTest() { |
||||||
|
// Given
|
||||||
|
Transcoding transcoding = new Transcoding(null, |
||||||
|
"test-transcoding", |
||||||
|
"mp3", |
||||||
|
"wav", |
||||||
|
"step1", |
||||||
|
"step2", |
||||||
|
"step3", |
||||||
|
true); |
||||||
|
|
||||||
|
transcodingService.createTranscoding(transcoding); |
||||||
|
verify(playerService).getAllPlayers(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
spring.mvc.view.prefix: /WEB-INF/jsp/ |
||||||
|
spring.mvc.view.suffix: .jsp |
||||||
|
server.error.includeStacktrace: ALWAYS |
||||||
|
logging.level.root=WARN |
||||||
|
logging.level.org.airsonic=INFO |
||||||
|
logging.level.liquibase=INFO |
||||||
|
logging.pattern.console=%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p){green} %clr(---){faint} %clr(%-40.40logger{32}){blue} %clr(:){faint} %m%n%wEx |
||||||
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- %-40.40logger{32} : %m%n%wEx |
||||||
|
DatabaseConfigType=embed |
||||||
|
DatabaseConfigEmbedDriver=org.hsqldb.jdbcDriver |
||||||
|
DatabaseConfigEmbedUrl=jdbc:hsqldb:mem:airsonic |
||||||
|
DatabaseConfigEmbedUsername=sa |
||||||
|
DatabaseConfigEmbedPassword= |
@ -0,0 +1,205 @@ |
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> |
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>airsonic-integration-test</artifactId> |
||||||
|
<name>Airsonic Integration Test</name> |
||||||
|
|
||||||
|
<parent> |
||||||
|
<groupId>org.airsonic.player</groupId> |
||||||
|
<artifactId>airsonic</artifactId> |
||||||
|
<version>10.2.0-SNAPSHOT</version> |
||||||
|
</parent> |
||||||
|
|
||||||
|
<properties> |
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||||
|
<cucumber.version>2.3.1</cucumber.version> |
||||||
|
</properties> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>io.cucumber</groupId> |
||||||
|
<artifactId>cucumber-core</artifactId> |
||||||
|
<version>${cucumber.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.cucumber</groupId> |
||||||
|
<artifactId>cucumber-junit</artifactId> |
||||||
|
<version>${cucumber.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.cucumber</groupId> |
||||||
|
<artifactId>cucumber-java</artifactId> |
||||||
|
<version>${cucumber.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.cucumber</groupId> |
||||||
|
<artifactId>cucumber-java8</artifactId> |
||||||
|
<version>${cucumber.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.cucumber</groupId> |
||||||
|
<artifactId>cucumber-spring</artifactId> |
||||||
|
<version>${cucumber.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-context</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-test</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>junit</groupId> |
||||||
|
<artifactId>junit</artifactId> |
||||||
|
<version>4.12</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.commons</groupId> |
||||||
|
<artifactId>commons-lang3</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>commons-codec</groupId> |
||||||
|
<artifactId>commons-codec</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>commons-io</groupId> |
||||||
|
<artifactId>commons-io</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.httpcomponents</groupId> |
||||||
|
<artifactId>httpcore</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.httpcomponents</groupId> |
||||||
|
<artifactId>httpclient</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.spotify</groupId> |
||||||
|
<artifactId>docker-client</artifactId> |
||||||
|
<version>8.13.1</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.slf4j</groupId> |
||||||
|
<artifactId>slf4j-api</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>ch.qos.logback</groupId> |
||||||
|
<artifactId>logback-classic</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.google.guava</groupId> |
||||||
|
<artifactId>guava</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.fasterxml.jackson.core</groupId> |
||||||
|
<artifactId>jackson-databind</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.xmlunit</groupId> |
||||||
|
<artifactId>xmlunit-core</artifactId> |
||||||
|
<version>2.6.0</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.xmlunit</groupId> |
||||||
|
<artifactId>xmlunit-matchers</artifactId> |
||||||
|
<version>2.6.0</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.airsonic.player</groupId> |
||||||
|
<artifactId>subsonic-rest-api</artifactId> |
||||||
|
<version>${project.version}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
|
||||||
|
<build> |
||||||
|
<testResources> |
||||||
|
<testResource> |
||||||
|
<directory>src/test/resources</directory> |
||||||
|
<filtering>true</filtering> |
||||||
|
</testResource> |
||||||
|
</testResources> |
||||||
|
<plugins> |
||||||
|
<plugin> |
||||||
|
<groupId>com.github.temyers</groupId> |
||||||
|
<artifactId>cucumber-jvm-parallel-plugin</artifactId> |
||||||
|
<version>5.0.0</version> |
||||||
|
<executions> |
||||||
|
<execution> |
||||||
|
<id>generateRunners</id> |
||||||
|
<phase>generate-test-sources</phase> |
||||||
|
<goals> |
||||||
|
<goal>generateRunners</goal> |
||||||
|
</goals> |
||||||
|
<configuration> |
||||||
|
<featuresDirectory>src/test/resources/features</featuresDirectory> |
||||||
|
<format>pretty</format> |
||||||
|
<glue>org.airsonic.test.cucumber.steps</glue> |
||||||
|
</configuration> |
||||||
|
</execution> |
||||||
|
</executions> |
||||||
|
</plugin> |
||||||
|
<plugin> |
||||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||||
|
<artifactId>maven-compiler-plugin</artifactId> |
||||||
|
<version>3.5.1</version> |
||||||
|
<configuration> |
||||||
|
<source>1.8</source> |
||||||
|
<target>1.8</target> |
||||||
|
<compilerVersion>1.8</compilerVersion> |
||||||
|
<showWarnings>true</showWarnings> |
||||||
|
<encoding>UTF-8</encoding> |
||||||
|
</configuration> |
||||||
|
</plugin> |
||||||
|
<plugin> |
||||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||||
|
<artifactId>maven-jar-plugin</artifactId> |
||||||
|
<version>3.1.0</version> |
||||||
|
<executions> |
||||||
|
<execution> |
||||||
|
<id>default-jar</id> |
||||||
|
<phase/> |
||||||
|
<configuration> |
||||||
|
<finalName>unwanted</finalName> |
||||||
|
<classifier>unwanted</classifier> |
||||||
|
</configuration> |
||||||
|
</execution> |
||||||
|
</executions> |
||||||
|
</plugin> |
||||||
|
<plugin> |
||||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||||
|
<artifactId>maven-surefire-plugin</artifactId> |
||||||
|
<version>2.21.0</version> |
||||||
|
<configuration> |
||||||
|
<parallel>all</parallel> |
||||||
|
<threadCount>2</threadCount> |
||||||
|
<includes> |
||||||
|
<include>**/Parallel*IT.class</include> |
||||||
|
</includes> |
||||||
|
</configuration> |
||||||
|
</plugin> |
||||||
|
</plugins> |
||||||
|
</build> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,12 @@ |
|||||||
|
package org.airsonic.test; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.ComponentScan; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.context.annotation.PropertySource; |
||||||
|
|
||||||
|
@Configuration |
||||||
|
@ComponentScan("org.airsonic.test") |
||||||
|
@PropertySource("classpath:application.properties") |
||||||
|
public class SpringContext { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package org.airsonic.test.cucumber; |
||||||
|
|
||||||
|
import cucumber.api.CucumberOptions; |
||||||
|
import cucumber.api.junit.Cucumber; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
|
||||||
|
@RunWith(Cucumber.class) |
||||||
|
@CucumberOptions(plugin = {"pretty"}, |
||||||
|
features = "classpath:features/api/stream-mp3.feature", |
||||||
|
glue = "org.airsonic.test.cucumber.steps") |
||||||
|
public class RunCukesTest { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package org.airsonic.test.cucumber.server; |
||||||
|
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils; |
||||||
|
import org.apache.http.client.methods.RequestBuilder; |
||||||
|
|
||||||
|
import java.nio.file.Path; |
||||||
|
|
||||||
|
public interface AirsonicServer { |
||||||
|
String getBaseUri(); |
||||||
|
|
||||||
|
void uploadToDefaultMusicFolder(Path directoryPath, String relativePath); |
||||||
|
|
||||||
|
default void addRestParameters(RequestBuilder builder) { |
||||||
|
builder.addParameter("c", "inttest"); |
||||||
|
builder.addParameter("v", "1.15.0"); |
||||||
|
builder.addParameter("u", "admin"); |
||||||
|
builder.addParameter("s", "int"); |
||||||
|
builder.addParameter("t", DigestUtils.md5Hex("admin" + "int")); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
package org.airsonic.test.cucumber.steps.api; |
||||||
|
|
||||||
|
import cucumber.api.java8.En; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.apache.http.HttpEntity; |
||||||
|
import org.apache.http.StatusLine; |
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse; |
||||||
|
import org.apache.http.client.methods.HttpGet; |
||||||
|
import org.apache.http.impl.client.CloseableHttpClient; |
||||||
|
import org.apache.http.impl.client.HttpClientBuilder; |
||||||
|
import org.apache.http.util.EntityUtils; |
||||||
|
import org.xmlunit.builder.Input; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertThat; |
||||||
|
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo; |
||||||
|
|
||||||
|
public class PingStepDef implements En { |
||||||
|
|
||||||
|
private CloseableHttpResponse response; |
||||||
|
private CloseableHttpClient client; |
||||||
|
|
||||||
|
public PingStepDef(AirsonicServer server) { |
||||||
|
this.client = HttpClientBuilder.create().build(); |
||||||
|
When("^A ping request is sent$", () -> { |
||||||
|
HttpGet httpGet = new HttpGet(server.getBaseUri() + "/rest/ping"); |
||||||
|
this.response = client.execute(httpGet); |
||||||
|
}); |
||||||
|
Then("^A required parameter response is received$", () -> { |
||||||
|
if(response == null) { |
||||||
|
throw new IllegalStateException(); |
||||||
|
} |
||||||
|
try { |
||||||
|
StatusLine statusLine = response.getStatusLine(); |
||||||
|
assertEquals(statusLine.getStatusCode(), 200); |
||||||
|
HttpEntity entity = response.getEntity(); |
||||||
|
String actual = EntityUtils.toString(entity); |
||||||
|
assertThat( |
||||||
|
actual, |
||||||
|
isIdenticalTo( |
||||||
|
Input.fromStream( |
||||||
|
getClass().getResourceAsStream("/blobs/ping/missing-auth.xml"))) |
||||||
|
.ignoreWhitespace()); |
||||||
|
} finally { |
||||||
|
response.close(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package org.airsonic.test.cucumber.steps.api; |
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||||
|
import com.fasterxml.jackson.databind.MapperFeature; |
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||||
|
import cucumber.api.java8.En; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse; |
||||||
|
import org.apache.http.client.methods.RequestBuilder; |
||||||
|
import org.apache.http.impl.client.CloseableHttpClient; |
||||||
|
import org.apache.http.impl.client.HttpClientBuilder; |
||||||
|
import org.apache.http.util.EntityUtils; |
||||||
|
import org.junit.Assert; |
||||||
|
import org.subsonic.restapi.Response; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
public class ScanStepDef implements En { |
||||||
|
|
||||||
|
private final AirsonicServer server; |
||||||
|
private CloseableHttpResponse response; |
||||||
|
private CloseableHttpClient client; |
||||||
|
private ObjectMapper mapper = new ObjectMapper(); |
||||||
|
|
||||||
|
public ScanStepDef(AirsonicServer server) { |
||||||
|
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); |
||||||
|
this.client = HttpClientBuilder.create().build(); |
||||||
|
this.server = server; |
||||||
|
|
||||||
|
Given("a scan is done", () -> { |
||||||
|
Assert.assertFalse(isScanning()); |
||||||
|
|
||||||
|
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/startScan"); |
||||||
|
server.addRestParameters(builder); |
||||||
|
response = client.execute(builder.build()); |
||||||
|
System.out.println(EntityUtils.toString(response.getEntity())); |
||||||
|
Long waitTime = 30000L; |
||||||
|
Long sleepTime = 1000L; |
||||||
|
while(waitTime > 0 && isScanning()) { |
||||||
|
waitTime -= sleepTime; |
||||||
|
Thread.sleep(sleepTime); |
||||||
|
} |
||||||
|
|
||||||
|
Assert.assertFalse(isScanning()); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private boolean isScanning() throws IOException { |
||||||
|
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/getScanStatus"); |
||||||
|
builder.addParameter("f", "json"); |
||||||
|
server.addRestParameters(builder); |
||||||
|
response = client.execute(builder.build()); |
||||||
|
|
||||||
|
String responseAsString = EntityUtils.toString(response.getEntity()); |
||||||
|
JsonNode jsonNode = mapper.readTree(responseAsString).get("subsonic-response"); |
||||||
|
Response response = mapper.treeToValue(jsonNode, Response.class); |
||||||
|
return response.getScanStatus().isScanning(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package org.airsonic.test.cucumber.steps.api; |
||||||
|
|
||||||
|
import cucumber.api.java.Before; |
||||||
|
import org.airsonic.test.SpringContext; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.springframework.test.context.ContextConfiguration; |
||||||
|
|
||||||
|
@ContextConfiguration(classes = SpringContext.class) |
||||||
|
public class SpringStepDef { |
||||||
|
|
||||||
|
public SpringStepDef(AirsonicServer server) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup_cucumber_spring_context(){ |
||||||
|
// Dummy method so cucumber will recognize this class as glue
|
||||||
|
// and use its context configuration.
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
package org.airsonic.test.cucumber.steps.api; |
||||||
|
|
||||||
|
import cucumber.api.java8.En; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.apache.commons.io.FileUtils; |
||||||
|
import org.apache.commons.io.HexDump; |
||||||
|
import org.apache.commons.io.IOUtils; |
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse; |
||||||
|
import org.apache.http.client.methods.RequestBuilder; |
||||||
|
import org.apache.http.impl.client.CloseableHttpClient; |
||||||
|
import org.apache.http.impl.client.HttpClientBuilder; |
||||||
|
import org.apache.http.util.EntityUtils; |
||||||
|
import org.junit.Assert; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.file.Paths; |
||||||
|
|
||||||
|
public class StreamStepDef implements En { |
||||||
|
|
||||||
|
private CloseableHttpResponse response; |
||||||
|
private CloseableHttpClient client; |
||||||
|
private boolean closed = false; |
||||||
|
private byte[] body; |
||||||
|
|
||||||
|
public StreamStepDef(AirsonicServer server) { |
||||||
|
this.client = HttpClientBuilder.create().build(); |
||||||
|
Given("Media file (.*) is added", (String mediaFile) -> { |
||||||
|
// TODO fix this
|
||||||
|
server.uploadToDefaultMusicFolder( |
||||||
|
Paths.get(this.getClass().getResource("/blobs/stream/piano").toURI()), |
||||||
|
""); |
||||||
|
}); |
||||||
|
|
||||||
|
When("A stream request is sent", () -> { |
||||||
|
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/stream"); |
||||||
|
builder.addParameter("id", "2"); |
||||||
|
builder.addHeader("Range", "bytes=0-"); |
||||||
|
builder.addHeader("Accept", "audio/webm,audio/ogg,audio/wav,audio/*;"); |
||||||
|
server.addRestParameters(builder); |
||||||
|
response = client.execute(builder.build()); |
||||||
|
}); |
||||||
|
|
||||||
|
Then("The response bytes are equal", () -> { |
||||||
|
ensureBodyRead(); |
||||||
|
|
||||||
|
FileUtils.writeByteArrayToFile(new File("/tmp/bytearray"), body); |
||||||
|
|
||||||
|
byte[] expected = IOUtils.toByteArray(this.getClass().getResource("/blobs/stream/piano/piano.mp3").toURI()); |
||||||
|
//
|
||||||
|
// HexDump.dump(expected, 0, System.out, 0);
|
||||||
|
|
||||||
|
Assert.assertArrayEquals(expected, body); |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void ensureBodyRead() throws IOException { |
||||||
|
if(closed) { |
||||||
|
return; |
||||||
|
} else { |
||||||
|
this.body = EntityUtils.toByteArray(response.getEntity()); |
||||||
|
closed = true; |
||||||
|
response.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
package org.airsonic.test.cucumber_hooks.docker; |
||||||
|
|
||||||
|
import com.spotify.docker.client.DefaultDockerClient; |
||||||
|
import com.spotify.docker.client.DockerClient; |
||||||
|
import com.spotify.docker.client.exceptions.DockerException; |
||||||
|
import com.spotify.docker.client.messages.*; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.apache.commons.lang3.RandomStringUtils; |
||||||
|
import org.apache.commons.lang3.StringUtils; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.beans.factory.DisposableBean; |
||||||
|
import org.springframework.beans.factory.InitializingBean; |
||||||
|
import org.springframework.context.EnvironmentAware; |
||||||
|
import org.springframework.context.annotation.Profile; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import static com.spotify.docker.client.DockerClient.RemoveContainerParam.*; |
||||||
|
|
||||||
|
@Component |
||||||
|
@Profile("dynamic") |
||||||
|
public class DynamicDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean, DisposableBean { |
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DynamicDockerHook.class); |
||||||
|
public static final String AIRSONIC_DOCKER_IMAGE = "airsonic.docker.image"; |
||||||
|
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port"; |
||||||
|
public static final String AIRSONIC_READY_MAX_WAIT = "airsonic.ready.max_wait"; |
||||||
|
public static final String AIRSONIC_READY_SLEEP_TIME = "airsonic.ready.sleep_time"; |
||||||
|
|
||||||
|
private String serverUri = null; |
||||||
|
private final DockerClient docker; |
||||||
|
private String containerId; |
||||||
|
private String dockerImage; |
||||||
|
private Integer dockerPort; |
||||||
|
private Long readyMaxWaitTime; |
||||||
|
private Long readySleepTime; |
||||||
|
|
||||||
|
public DynamicDockerHook() { |
||||||
|
logger.debug("Using hook for dynamically creating containers"); |
||||||
|
docker = new DefaultDockerClient("unix:///var/run/docker.sock"); |
||||||
|
testDockerIsAvail(); |
||||||
|
} |
||||||
|
|
||||||
|
private void testDockerIsAvail() { |
||||||
|
try { |
||||||
|
logger.trace("Trying to ping docker daemon"); |
||||||
|
docker.ping(); |
||||||
|
} catch (Exception e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getBaseUri() { |
||||||
|
if (serverUri == null) { |
||||||
|
throw new IllegalStateException("Server is not yet ready"); |
||||||
|
} else { |
||||||
|
return serverUri; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) { |
||||||
|
try { |
||||||
|
// TODO ensure localDir is a directory
|
||||||
|
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath); |
||||||
|
} catch (DockerException | IOException | InterruptedException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void startServer() { |
||||||
|
logger.debug("Starting server"); |
||||||
|
final ContainerConfig config = ContainerConfig.builder() |
||||||
|
.image(dockerImage) |
||||||
|
.build(); |
||||||
|
|
||||||
|
final String name = "airsonic-it-" + RandomStringUtils.randomAlphabetic(10); |
||||||
|
try { |
||||||
|
final ContainerCreation containerCreate = docker.createContainer(config, name); |
||||||
|
containerId = containerCreate.id(); |
||||||
|
docker.startContainer(containerId); |
||||||
|
Long waitTime = readyMaxWaitTime; |
||||||
|
while(true) { |
||||||
|
ContainerInfo containerInfo = docker.inspectContainer(containerId); |
||||||
|
ContainerState.Health health = containerInfo.state().health(); |
||||||
|
if (health != null && StringUtils.equalsIgnoreCase(health.status(), "healthy")) { |
||||||
|
logger.trace("Container started early. Yay!"); |
||||||
|
break; |
||||||
|
} else if(waitTime > readySleepTime) { |
||||||
|
if(logger.isTraceEnabled()) { |
||||||
|
String message; |
||||||
|
if(health != null) { |
||||||
|
message = "Container ("+name+") not yet ready. State was: " + health.status(); |
||||||
|
} else { |
||||||
|
message = "Container ("+name+") state unknown. Waiting"; |
||||||
|
} |
||||||
|
logger.trace(message); |
||||||
|
} |
||||||
|
waitTime -= readySleepTime; |
||||||
|
Thread.sleep(readySleepTime); |
||||||
|
} else if(health == null) { |
||||||
|
logger.trace("Max wait time with unknown container state. Hoping container is ready"); |
||||||
|
break; |
||||||
|
} else { |
||||||
|
logger.trace("Container ("+name+") never became ready within max wait time"); |
||||||
|
throw new RuntimeException("Container ("+name+") not ready"); |
||||||
|
} |
||||||
|
} |
||||||
|
ContainerInfo containerInfo = docker.inspectContainer(containerId); |
||||||
|
try { |
||||||
|
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next(); |
||||||
|
String ipAddress = next.getValue().ipAddress(); |
||||||
|
serverUri = "http://" + ipAddress + ":" + dockerPort; |
||||||
|
} catch(Exception e) { |
||||||
|
throw new RuntimeException("Could not determine container ("+name+") address", e); |
||||||
|
} |
||||||
|
} catch (DockerException | InterruptedException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void stopServer() { |
||||||
|
if(containerId != null) { |
||||||
|
try { |
||||||
|
docker.removeContainer(containerId, forceKill(), removeVolumes()); |
||||||
|
} catch (DockerException | InterruptedException e) { |
||||||
|
throw new RuntimeException("Could not remove container", e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setEnvironment(Environment environment) { |
||||||
|
dockerImage = environment.getRequiredProperty(AIRSONIC_DOCKER_IMAGE); |
||||||
|
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT)); |
||||||
|
readyMaxWaitTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_MAX_WAIT)); |
||||||
|
readySleepTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_SLEEP_TIME)); |
||||||
|
if(readyMaxWaitTime <= 0L || readySleepTime <= 0L) { |
||||||
|
throw new IllegalArgumentException("Max wait time and sleep time must be greater than 0"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void destroy() throws Exception { |
||||||
|
stopServer(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void afterPropertiesSet() throws Exception { |
||||||
|
startServer(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
package org.airsonic.test.cucumber_hooks.docker; |
||||||
|
|
||||||
|
import com.spotify.docker.client.DefaultDockerClient; |
||||||
|
import com.spotify.docker.client.DockerClient; |
||||||
|
import com.spotify.docker.client.exceptions.DockerException; |
||||||
|
import com.spotify.docker.client.messages.AttachedNetwork; |
||||||
|
import com.spotify.docker.client.messages.ContainerInfo; |
||||||
|
import org.airsonic.test.cucumber.server.AirsonicServer; |
||||||
|
import org.apache.commons.lang3.StringUtils; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.beans.factory.InitializingBean; |
||||||
|
import org.springframework.context.EnvironmentAware; |
||||||
|
import org.springframework.context.annotation.Profile; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
@Component |
||||||
|
@Profile("existing") |
||||||
|
public class ExistingDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean { |
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ExistingDockerHook.class); |
||||||
|
public static final String AIRSONIC_DOCKER_CONTAINER = "airsonic.docker.container"; |
||||||
|
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port"; |
||||||
|
|
||||||
|
private String serverUri = null; |
||||||
|
private final DockerClient docker; |
||||||
|
private String containerId; |
||||||
|
private Integer dockerPort; |
||||||
|
|
||||||
|
public ExistingDockerHook() { |
||||||
|
logger.debug("Using hook for existing docker container"); |
||||||
|
docker = new DefaultDockerClient("unix:///var/run/docker.sock"); |
||||||
|
testDockerIsAvail(); |
||||||
|
} |
||||||
|
|
||||||
|
private void testDockerIsAvail() { |
||||||
|
try { |
||||||
|
logger.trace("Trying to ping docker daemon"); |
||||||
|
docker.ping(); |
||||||
|
} catch (Exception e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getBaseUri() { |
||||||
|
if (serverUri == null) { |
||||||
|
throw new IllegalStateException("Server is not yet ready"); |
||||||
|
} else { |
||||||
|
return serverUri; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) { |
||||||
|
try { |
||||||
|
// TODO ensure localDir is a directory
|
||||||
|
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath); |
||||||
|
} catch (DockerException | IOException | InterruptedException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void connectToServer() { |
||||||
|
logger.debug("Connecting to server"); |
||||||
|
|
||||||
|
try { |
||||||
|
ContainerInfo containerInfo = docker.inspectContainer(containerId); |
||||||
|
if(!containerInfo.state().running()) { |
||||||
|
throw new IllegalStateException("Container is not running " + containerId); |
||||||
|
} |
||||||
|
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next(); |
||||||
|
String ipAddress = next.getValue().ipAddress(); |
||||||
|
if(StringUtils.isBlank(ipAddress)) { |
||||||
|
throw new IllegalStateException("No address found for container " + containerId); |
||||||
|
} |
||||||
|
serverUri = "http://" + ipAddress + ":" + dockerPort; |
||||||
|
} catch (DockerException | InterruptedException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setEnvironment(Environment environment) { |
||||||
|
containerId = environment.getRequiredProperty(AIRSONIC_DOCKER_CONTAINER); |
||||||
|
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void afterPropertiesSet() throws Exception { |
||||||
|
connectToServer(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
airsonic.docker.port=4040 |
||||||
|
airsonic.ready.max_wait=80000 |
||||||
|
airsonic.ready.sleep_time=5000 |
||||||
|
|
||||||
|
# Use for dynamically generating a container as needed |
||||||
|
spring.profiles.active=dynamic |
||||||
|
airsonic.docker.image=airsonic/airsonic:${project.version} |
||||||
|
|
||||||
|
# Use for testing against an existing/running container |
||||||
|
#airsonic.docker.container=1212be8a94e0 |
||||||
|
#spring.profiles.active=existing |
||||||
|
|
@ -0,0 +1,4 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<subsonic-response xmlns="http://subsonic.org/restapi" status="failed" version="1.15.0"> |
||||||
|
<error code="10" message="Required parameter is missing."/> |
||||||
|
</subsonic-response> |
@ -0,0 +1,2 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.15.0"/> |
Binary file not shown.
@ -0,0 +1,5 @@ |
|||||||
|
Feature: Ping API |
||||||
|
|
||||||
|
Scenario: Airsonic responds to ping requests |
||||||
|
When A ping request is sent |
||||||
|
Then A required parameter response is received |
@ -0,0 +1,11 @@ |
|||||||
|
Feature: Stream API for MP3 |
||||||
|
|
||||||
|
Background: |
||||||
|
Given Media file stream/piano/piano.mp3 is added |
||||||
|
And a scan is done |
||||||
|
|
||||||
|
Scenario: Airsonic sends stream data |
||||||
|
When A stream request is sent |
||||||
|
Then The response bytes are equal |
||||||
|
# TODO check length |
||||||
|
|
@ -0,0 +1,14 @@ |
|||||||
|
<configuration> |
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||||
|
<encoder> |
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
||||||
|
</encoder> |
||||||
|
</appender> |
||||||
|
|
||||||
|
<logger level="trace" name="org.airsonic.test.cucumber_hooks.docker.DockerHook" /> |
||||||
|
|
||||||
|
<root level="info"> |
||||||
|
<appender-ref ref="STDOUT" /> |
||||||
|
</root> |
||||||
|
</configuration> |
Loading…
Reference in new issue