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/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