diff --git a/.travis.yml b/.travis.yml index f0db4f2c..f9183d47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: java sudo: required jdk: - oraclejdk8 +services: + - docker cache: directories: - $HOME/.m2 @@ -13,4 +15,4 @@ before_script: - export M2_HOME=$PWD/apache-maven-3.5.4 - export PATH=$PWD/apache-maven-3.5.4/bin:$PATH script: - - mvn package install + - mvn verify -P integration-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 105f04c9..6543c6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ Note that with this release, the jdbc-extra flavored war is now the default and * New login page * Added additional war with builtin support for external databases * Improved playlist handling - * DLNA browsing improvments + * DLNA browsing improvements * Small fixes and improvements ## v6.2 diff --git a/README.md b/README.md index 3f589bc3..858cf46a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Airsonic What is Airsonic? ----------------- -Airsonic is a free, web-based media streamer, providing ubiquitious access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room. +Airsonic is a free, web-based media streamer, providing ubiquitous access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room. Airsonic is designed to handle very large music collections (hundreds of gigabytes). Although optimized for MP3 streaming, it works for any audio or video format that can stream over HTTP, for instance AAC and OGG. By using transcoder plug-ins, Airsonic supports on-the-fly conversion and streaming of virtually any audio format, including WMA, FLAC, APE, Musepack, WavPack and Shorten. diff --git a/airsonic-main/cve-suppressed.xml b/airsonic-main/cve-suppressed.xml index 4e3266b3..e947d376 100644 --- a/airsonic-main/cve-suppressed.xml +++ b/airsonic-main/cve-suppressed.xml @@ -124,6 +124,11 @@ ^org\.postgresql:postgresql:.*$ CVE-2018-1115 + + Does not affect the postgres client + ^org\.postgresql:postgresql:.*$ + CVE-2016-7048 + This is for nodejs ^org\.mariadb\.jdbc:mariadb-java-client:.*$ diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 7204c4fd..f64b7576 100755 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -15,6 +15,7 @@ 3.1.0 1.2.1-RELEASE + provided @@ -86,6 +87,16 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + org.springframework.security spring-security-ldap @@ -303,6 +314,10 @@ 1.10.19 test + + org.assertj + assertj-core + @@ -445,12 +460,12 @@ org.springframework.boot spring-boot-starter-tomcat - provided + ${tomcat.server.scope} org.apache.tomcat.embed tomcat-embed-jasper - provided + ${tomcat.server.scope} @@ -599,5 +614,11 @@ spring-boot-starter-tomcat + + ide-tomcat-embed + + compile + + diff --git a/airsonic-main/src/main/java/org/airsonic/player/Application.java b/airsonic-main/src/main/java/org/airsonic/player/Application.java index 3f476853..25857afe 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/Application.java +++ b/airsonic-main/src/main/java/org/airsonic/player/Application.java @@ -189,6 +189,7 @@ public class Application extends SpringBootServletInitializer implements Embedde @Override public void customize(ConfigurableEmbeddedServletContainer container) { + LOG.trace("Servlet container is {}", container.getClass().getCanonicalName()); // Yes, there is a good reason we do this. // We cannot count on the tomcat classes being on the classpath which will // happen if the war is deployed to another app server like Jetty. So, we @@ -197,6 +198,7 @@ public class Application extends SpringBootServletInitializer implements Embedde try { Class tomcatESCF = Class.forName("org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory"); if(tomcatESCF.isInstance(container)) { + LOG.info("Detected Tomcat web server"); LOG.debug("Attempting to optimize tomcat"); Object tomcatESCFInstance = tomcatESCF.cast(container); Class tomcatApplicationClass = Class.forName("org.airsonic.player.TomcatApplication"); @@ -207,10 +209,19 @@ public class Application extends SpringBootServletInitializer implements Embedde LOG.debug("Skipping tomcat optimization as we are not running on tomcat"); } } catch (NoClassDefFoundError | ClassNotFoundException e) { - LOG.debug("Skipping tomcat optimization as the tomcat classes are not available"); + LOG.debug("No tomcat classes found"); } catch (Exception e) { LOG.warn("An error happened while trying to optimize tomcat", e); } + + try { + Class jettyESCF = Class.forName("org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory"); + if(jettyESCF.isInstance(container)) { + LOG.warn("Detected Jetty web server. Here there be dragons."); + } + } catch (NoClassDefFoundError | ClassNotFoundException e) { + LOG.debug("No jetty classes found"); + } } public static void main(String[] args) { diff --git a/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java b/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java index 8cf23390..d62dea83 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java +++ b/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java @@ -3,6 +3,7 @@ package org.airsonic.player; import org.apache.catalina.Container; import org.apache.catalina.Wrapper; import org.apache.catalina.webresources.StandardRoot; +import org.apache.tomcat.util.scan.StandardJarScanFilter; import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; @@ -12,6 +13,11 @@ public class TomcatApplication { tomcatFactory.addContextCustomizers((TomcatContextCustomizer) context -> { + StandardJarScanFilter standardJarScanFilter = new StandardJarScanFilter(); + standardJarScanFilter.setTldScan("dwr-*.jar,jstl-*.jar,spring-security-taglibs-*.jar,spring-web-*.jar,spring-webmvc-*.jar,string-*.jar,taglibs-standard-impl-*.jar,tomcat-annotations-api-*.jar,tomcat-embed-jasper-*.jar"); + standardJarScanFilter.setTldSkip("*"); + context.getJarScanner().setJarScanFilter(standardJarScanFilter); + boolean development = (System.getProperty("airsonic.development") != null); // Increase the size and time before eviction of the Tomcat diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java index 73ab02aa..ee193430 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java @@ -1274,7 +1274,7 @@ public class SubsonicRESTController { child.setSuffix(suffix); child.setContentType(StringUtil.getMimeType(suffix)); child.setIsVideo(mediaFile.isVideo()); - child.setPath(getRelativePath(mediaFile)); + child.setPath(getRelativePath(mediaFile, settingsService)); org.airsonic.player.domain.Bookmark bookmark = bookmarkCache.get(new BookmarkKey(username, mediaFile.getId())); if (bookmark != null) { @@ -1329,7 +1329,7 @@ public class SubsonicRESTController { return null; } - private String getRelativePath(MediaFile musicFile) { + public static String getRelativePath(MediaFile musicFile, SettingsService settingsService) { String filePath = musicFile.getPath(); diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java index 17ad5943..2b59e321 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java +++ b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java @@ -22,6 +22,7 @@ package org.airsonic.player.dao; import org.airsonic.player.domain.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -43,6 +44,9 @@ public class PlayerDao extends AbstractDao { "last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id, mixer"; private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS; + @Autowired + private PlayerDaoPlayQueueFactory playerDaoPlayQueueFactory; + private PlayerRowMapper rowMapper = new PlayerRowMapper(); private Map playlists = Collections.synchronizedMap(new HashMap()); @@ -166,7 +170,7 @@ public class PlayerDao extends AbstractDao { private void addPlaylist(Player player) { PlayQueue playQueue = playlists.get(player.getId()); if (playQueue == null) { - playQueue = new PlayQueue(); + playQueue = playerDaoPlayQueueFactory.createPlayQueue(); playlists.put(player.getId(), playQueue); } player.setPlayQueue(playQueue); diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java new file mode 100644 index 00000000..ceb65087 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java @@ -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(); + } +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java index 59668a0a..8b6c8b7e 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java @@ -1,9 +1,9 @@ package org.airsonic.player.service; -import com.github.biconou.AudioPlayer.JavaPlayer; -import com.github.biconou.AudioPlayer.api.*; +import com.github.biconou.AudioPlayer.api.PlayList; +import com.github.biconou.AudioPlayer.api.PlayerListener; import org.airsonic.player.domain.*; -import org.airsonic.player.domain.Player; +import org.airsonic.player.service.jukebox.JavaPlayerFactory; import org.airsonic.player.util.FileUtil; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; @@ -19,23 +19,25 @@ import java.util.Map; /** - * @author R??mi Cocula + * @author Rémi Cocula */ @Service public class JukeboxJavaService { private static final org.slf4j.Logger log = LoggerFactory.getLogger(JukeboxJavaService.class); + private static final float DEFAULT_GAIN = 0.75f; + @Autowired private AudioScrobblerService audioScrobblerService; @Autowired private StatusService statusService; @Autowired - private SettingsService settingsService; - @Autowired private SecurityService securityService; @Autowired private MediaFileService mediaFileService; + @Autowired + private JavaPlayerFactory javaPlayerFactory; private TransferStatus status; @@ -45,31 +47,32 @@ public class JukeboxJavaService { /** * Finds the corresponding active audio player for a given airsonic player. + * If no player exists we create one. * The JukeboxJavaService references all active audio players in a map indexed by airsonic player id. * * @param airsonicPlayer a given airsonic player. - * @return the corresponding active audio player of null if none exists. + * @return the corresponding active audio player. */ private com.github.biconou.AudioPlayer.api.Player retrieveAudioPlayerForAirsonicPlayer(Player airsonicPlayer) { com.github.biconou.AudioPlayer.api.Player foundPlayer = activeAudioPlayers.get(airsonicPlayer.getId()); if (foundPlayer == null) { synchronized (activeAudioPlayers) { - foundPlayer = initAudioPlayer(airsonicPlayer); - if (foundPlayer == null) { + com.github.biconou.AudioPlayer.api.Player newPlayer = initAudioPlayer(airsonicPlayer); + if (newPlayer == null) { throw new RuntimeException("Did not initialized a player"); - } else { - activeAudioPlayers.put(airsonicPlayer.getId(), foundPlayer); - String mixer = airsonicPlayer.getJavaJukeboxMixer(); - if (StringUtils.isBlank(mixer)) { - mixer = DEFAULT_MIXER_ENTRY_KEY; - } - List playersForMixer = activeAudioPlayersPerMixer.get(mixer); - if (playersForMixer == null) { - playersForMixer = new ArrayList<>(); - activeAudioPlayersPerMixer.put(mixer, playersForMixer); - } - playersForMixer.add(foundPlayer); } + activeAudioPlayers.put(airsonicPlayer.getId(), newPlayer); + String mixer = airsonicPlayer.getJavaJukeboxMixer(); + if (StringUtils.isBlank(mixer)) { + mixer = DEFAULT_MIXER_ENTRY_KEY; + } + List playersForMixer = activeAudioPlayersPerMixer.get(mixer); + if (playersForMixer == null) { + playersForMixer = new ArrayList<>(); + activeAudioPlayersPerMixer.put(mixer, playersForMixer); + } + playersForMixer.add(newPlayer); + foundPlayer = newPlayer; } } return foundPlayer; @@ -88,11 +91,12 @@ public class JukeboxJavaService { if (StringUtils.isNotBlank(airsonicPlayer.getJavaJukeboxMixer())) { log.info("use mixer : {}", airsonicPlayer.getJavaJukeboxMixer()); - audioPlayer = new JavaPlayer(airsonicPlayer.getJavaJukeboxMixer()); + audioPlayer = javaPlayerFactory.createJavaPlayer(airsonicPlayer.getJavaJukeboxMixer()); } else { log.info("use default mixer"); - audioPlayer = new JavaPlayer(); + audioPlayer = javaPlayerFactory.createJavaPlayer(); } + audioPlayer.setGain(DEFAULT_GAIN); if (audioPlayer != null) { audioPlayer.registerListener(new PlayerListener() { @Override @@ -159,10 +163,7 @@ public class JukeboxJavaService { throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player"); } com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer); - if (audioPlayer != null) { - return audioPlayer.getGain(); - } - return 0.5f; + return audioPlayer.getGain(); } public void setGain(final Player airsonicPlayer, final float gain) { @@ -271,20 +272,8 @@ public class JukeboxJavaService { } } - public void start(Player airsonicPlayer) throws Exception { - log.debug("begin start jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName()); - - com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer); - - // Control user authorizations - User user = securityService.getUserByName(airsonicPlayer.getUsername()); - if (!user.isJukeboxRole()) { - log.warn("{} is not authorized for jukebox playback.", user.getUsername()); - return; - } - - log.debug("PlayQueue.Status is {}", airsonicPlayer.getPlayQueue().getStatus()); - audioPlayer.play(); + public void start(Player airsonicPlayer) { + play(airsonicPlayer); } public void stop(Player airsonicPlayer) throws Exception { @@ -332,7 +321,6 @@ public class JukeboxJavaService { } } - public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { this.audioScrobblerService = audioScrobblerService; } @@ -341,10 +329,6 @@ public class JukeboxJavaService { this.statusService = statusService; } - public void setSettingsService(SettingsService settingsService) { - this.settingsService = settingsService; - } - public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java index 4205bf43..939d07c5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java @@ -21,6 +21,7 @@ package org.airsonic.player.service; import org.airsonic.player.domain.*; import org.airsonic.player.service.jukebox.AudioPlayer; +import org.airsonic.player.service.jukebox.AudioPlayerFactory; import org.airsonic.player.util.FileUtil; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; @@ -52,6 +53,8 @@ public class JukeboxLegacySubsonicService implements AudioPlayer.Listener { private SecurityService securityService; @Autowired private MediaFileService mediaFileService; + @Autowired + private AudioPlayerFactory audioPlayerFactory; private AudioPlayer audioPlayer; private Player player; @@ -111,7 +114,7 @@ public class JukeboxLegacySubsonicService implements AudioPlayer.Listener { String command = settingsService.getJukeboxCommand(); parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false)); in = transcodingService.getTranscodedInputStream(parameters); - audioPlayer = new AudioPlayer(in, this); + audioPlayer = audioPlayerFactory.createAudioPlayer(in, this); audioPlayer.setGain(gain); audioPlayer.play(); onSongStart(file); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java index 21499ee8..9176b281 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java @@ -32,6 +32,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import java.io.File; @@ -61,6 +62,7 @@ public class TranscodingService { @Autowired private SettingsService settingsService; @Autowired + @Lazy // used to deal with circular dependencies between PlayerService and TranscodingService private PlayerService playerService; /** diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java new file mode 100644 index 00000000..a7253011 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java @@ -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); + } +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java new file mode 100644 index 00000000..7ed6b2ed --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java @@ -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); + } +} diff --git a/airsonic-main/src/main/resources/application.properties b/airsonic-main/src/main/resources/application.properties index 3ab7c4db..a536e667 100644 --- a/airsonic-main/src/main/resources/application.properties +++ b/airsonic-main/src/main/resources/application.properties @@ -6,3 +6,6 @@ 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 + +# Helpful to debug which jars are scanned +#logging.level.org.apache.tomcat.util.scan=TRACE diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index f2945d5c..c54a3d18 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -279,7 +279,7 @@ help.homepage.title=Homepage help.forum.title=Forum help.shop.title=Merchandise help.contact.title=Contact -help.contact.text=Airsonic is a community project. You can find us in #airsonic on Freenode. Technical issues can be submitted to the issue tracker on GitHub. +help.contact.text=Airsonic is a community project. You can find us in #airsonic on Freenode. Technical issues can be submitted to the issue tracker on GitHub. help.log=Log help.logfile=The complete log is saved in {0}. # settingsHeader.jsp diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties index 27f81f5e..c6304ccb 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties @@ -279,7 +279,7 @@ help.homepage.title=\u9996\u9801 help.forum.title=\u8AD6\u58C7 help.shop.title=\u5546\u54C1 help.contact.title=\u806F\u7E6B -help.contact.text=Airsonic \u662F\u793E\u7FA4\u5C08\u6848\u3002 \u60A8\u53EF\u4EE5\u5728 Freenode \u4E0A\u7684 #airsonic \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668\u3002 +help.contact.text=Airsonic \u662F\u793E\u7FA4\u5C08\u6848\u3002 \u60A8\u53EF\u4EE5\u5728 Freenode \u4E0A\u7684 #airsonic \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668\u3002 help.log=\u8A18\u9304 help.logfile=\u5B8C\u6574\u7684\u7D00\u9304\u5B58\u653E\u5728 {0}\u3002 # settingsHeader.jsp diff --git a/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java b/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java index 12b102a0..50c8b5d0 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java +++ b/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java @@ -1,5 +1,6 @@ package org.airsonic.player; +import org.airsonic.player.controller.JAXBWriter; import org.airsonic.player.dao.DaoHelper; import org.airsonic.player.service.MediaScannerService; import org.apache.commons.io.FileUtils; @@ -37,6 +38,13 @@ public class TestCaseUtils { return airsonicHomeDirForTest.getAbsolutePath(); } + /** + * + * @return current REST api version. + */ + public static String restApiVersion() { + return new JAXBWriter().getRestProtocolVersion(); + } /** * Cleans the AIRSONIC_HOME directory used for tests. @@ -106,6 +114,7 @@ public class TestCaseUtils { * Scans the music library * @param mediaScannerService */ public static void execScan(MediaScannerService mediaScannerService) { + // TODO create a synchronous scan mediaScannerService.scanLibrary(); while (mediaScannerService.isScanning()) { diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java new file mode 100644 index 00000000..0dce645d --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java @@ -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()); + } +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java new file mode 100644 index 00000000..0195fedd --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java @@ -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. + * + *
    + *
  • Creates 2 music folder
  • + *
  • Scans the music folders
  • + *
  • Creates a test jukebox player
  • + *
+ */ + 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")); + } +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java new file mode 100644 index 00000000..f0c61a0b --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java @@ -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); + } + +} \ No newline at end of file diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java new file mode 100644 index 00000000..2cdee852 --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java @@ -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(); + } + +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java new file mode 100644 index 00000000..1c9293b4 --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java @@ -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(); + } +} diff --git a/airsonic-main/src/test/resources/application.properties b/airsonic-main/src/test/resources/application.properties new file mode 100644 index 00000000..29c1c5dd --- /dev/null +++ b/airsonic-main/src/test/resources/application.properties @@ -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= \ No newline at end of file diff --git a/install/docker/Dockerfile b/install/docker/Dockerfile index 83068a77..d3a00260 100644 --- a/install/docker/Dockerfile +++ b/install/docker/Dockerfile @@ -16,6 +16,7 @@ RUN apk --no-cache add \ ttf-dejavu \ ca-certificates \ tini \ + curl \ openjdk8-jre COPY run.sh /usr/local/bin/run.sh @@ -28,6 +29,6 @@ EXPOSE $AIRSONIC_PORT VOLUME $AIRSONIC_DIR/data $AIRSONIC_DIR/music $AIRSONIC_DIR/playlists $AIRSONIC_DIR/podcasts -HEALTHCHECK CMD wget -q http://localhost:"$AIRSONIC_PORT""$CONTEXT_PATH"/rest/ping -O /dev/null || exit 1 +HEALTHCHECK --interval=15s --timeout=3s CMD wget -q http://localhost:"$AIRSONIC_PORT""$CONTEXT_PATH"rest/ping -O /dev/null || exit 1 ENTRYPOINT ["tini", "--", "run.sh"] diff --git a/integration-test/pom.xml b/integration-test/pom.xml new file mode 100644 index 00000000..00f96fc3 --- /dev/null +++ b/integration-test/pom.xml @@ -0,0 +1,205 @@ + + + 4.0.0 + airsonic-integration-test + Airsonic Integration Test + + + org.airsonic.player + airsonic + 10.2.0-SNAPSHOT + + + + UTF-8 + 2.3.1 + + + + + io.cucumber + cucumber-core + ${cucumber.version} + test + + + io.cucumber + cucumber-junit + ${cucumber.version} + test + + + io.cucumber + cucumber-java + ${cucumber.version} + test + + + io.cucumber + cucumber-java8 + ${cucumber.version} + test + + + io.cucumber + cucumber-spring + ${cucumber.version} + test + + + org.springframework + spring-context + test + + + org.springframework + spring-test + + + junit + junit + 4.12 + test + + + org.apache.commons + commons-lang3 + test + + + commons-codec + commons-codec + test + + + commons-io + commons-io + test + + + org.apache.httpcomponents + httpcore + test + + + org.apache.httpcomponents + httpclient + test + + + com.spotify + docker-client + 8.13.1 + test + + + org.slf4j + slf4j-api + test + + + ch.qos.logback + logback-classic + test + + + com.google.guava + guava + test + + + com.fasterxml.jackson.core + jackson-databind + test + + + org.xmlunit + xmlunit-core + 2.6.0 + test + + + org.xmlunit + xmlunit-matchers + 2.6.0 + test + + + org.airsonic.player + subsonic-rest-api + ${project.version} + test + + + + + + + src/test/resources + true + + + + + com.github.temyers + cucumber-jvm-parallel-plugin + 5.0.0 + + + generateRunners + generate-test-sources + + generateRunners + + + src/test/resources/features + pretty + org.airsonic.test.cucumber.steps + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + 1.8 + true + UTF-8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + default-jar + + + unwanted + unwanted + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.21.0 + + all + 2 + + **/Parallel*IT.class + + + + + + + diff --git a/integration-test/src/test/java/org/airsonic/test/SpringContext.java b/integration-test/src/test/java/org/airsonic/test/SpringContext.java new file mode 100644 index 00000000..e38c6aef --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/SpringContext.java @@ -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 { + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java b/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java new file mode 100644 index 00000000..821959ac --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java @@ -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 { + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java b/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java new file mode 100644 index 00000000..b3e652d4 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java @@ -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")); + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java new file mode 100644 index 00000000..c66142db --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java @@ -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(); + } + }); + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java new file mode 100644 index 00000000..784e43e6 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java @@ -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(); + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java new file mode 100644 index 00000000..b1ff303f --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java @@ -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. + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java new file mode 100644 index 00000000..39aedfb4 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java @@ -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(); + } + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java new file mode 100644 index 00000000..4c08de5d --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java @@ -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 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(); + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java new file mode 100644 index 00000000..27592917 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java @@ -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 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(); + } +} diff --git a/integration-test/src/test/resources/application.properties b/integration-test/src/test/resources/application.properties new file mode 100644 index 00000000..95c1423a --- /dev/null +++ b/integration-test/src/test/resources/application.properties @@ -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 + diff --git a/integration-test/src/test/resources/blobs/ping/missing-auth.xml b/integration-test/src/test/resources/blobs/ping/missing-auth.xml new file mode 100644 index 00000000..7199e614 --- /dev/null +++ b/integration-test/src/test/resources/blobs/ping/missing-auth.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/integration-test/src/test/resources/blobs/ping/ok.xml b/integration-test/src/test/resources/blobs/ping/ok.xml new file mode 100644 index 00000000..54302310 --- /dev/null +++ b/integration-test/src/test/resources/blobs/ping/ok.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 b/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 new file mode 100644 index 00000000..dd253d5d Binary files /dev/null and b/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 differ diff --git a/integration-test/src/test/resources/features/api/ping.feature b/integration-test/src/test/resources/features/api/ping.feature new file mode 100644 index 00000000..ff403766 --- /dev/null +++ b/integration-test/src/test/resources/features/api/ping.feature @@ -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 diff --git a/integration-test/src/test/resources/features/api/stream-mp3.feature b/integration-test/src/test/resources/features/api/stream-mp3.feature new file mode 100644 index 00000000..86fa4358 --- /dev/null +++ b/integration-test/src/test/resources/features/api/stream-mp3.feature @@ -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 + diff --git a/integration-test/src/test/resources/logback.xml b/integration-test/src/test/resources/logback.xml new file mode 100644 index 00000000..590cf2fc --- /dev/null +++ b/integration-test/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index be66c6d3..2dcd4e7b 100644 --- a/pom.xml +++ b/pom.xml @@ -116,12 +116,22 @@ com.fasterxml.jackson.core jackson-core - 2.8.11 + 2.9.6 com.fasterxml.jackson.core jackson-databind - 2.8.11.1 + 2.9.6 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.0 + + + com.google.guava + guava + 20.0
@@ -303,6 +313,16 @@ install/docker + + integration-test + + subsonic-rest-api + airsonic-sonos-api + airsonic-main + install/docker + integration-test + + sign