Added docker based integration testing
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>airsonic-integration-test</artifactId>
|
||||
<name>Airsonic Integration Test</name>
|
||||
|
||||
<parent>
|
||||
<groupId>org.airsonic.player</groupId>
|
||||
<artifactId>airsonic</artifactId>
|
||||
<version>10.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<cucumber.version>2.3.1</cucumber.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-core</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-junit</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-java8</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-spring</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.spotify</groupId>
|
||||
<artifactId>docker-client</artifactId>
|
||||
<version>8.13.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xmlunit</groupId>
|
||||
<artifactId>xmlunit-core</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xmlunit</groupId>
|
||||
<artifactId>xmlunit-matchers</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.airsonic.player</groupId>
|
||||
<artifactId>subsonic-rest-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</testResource>
|
||||
</testResources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.temyers</groupId>
|
||||
<artifactId>cucumber-jvm-parallel-plugin</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generateRunners</id>
|
||||
<phase>generate-test-sources</phase>
|
||||
<goals>
|
||||
<goal>generateRunners</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<featuresDirectory>src/test/resources/features</featuresDirectory>
|
||||
<format>pretty</format>
|
||||
<glue>org.airsonic.test.cucumber.steps</glue>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerVersion>1.8</compilerVersion>
|
||||
<showWarnings>true</showWarnings>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-jar</id>
|
||||
<phase/>
|
||||
<configuration>
|
||||
<finalName>unwanted</finalName>
|
||||
<classifier>unwanted</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.21.0</version>
|
||||
<configuration>
|
||||
<parallel>all</parallel>
|
||||
<threadCount>2</threadCount>
|
||||
<includes>
|
||||
<include>**/Parallel*IT.class</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.airsonic.test;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("org.airsonic.test")
|
||||
@PropertySource("classpath:application.properties")
|
||||
public class SpringContext {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.airsonic.test.cucumber;
|
||||
|
||||
import cucumber.api.CucumberOptions;
|
||||
import cucumber.api.junit.Cucumber;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(Cucumber.class)
|
||||
@CucumberOptions(plugin = {"pretty"},
|
||||
features = "classpath:features/api/stream-mp3.feature",
|
||||
glue = "org.airsonic.test.cucumber.steps")
|
||||
public class RunCukesTest {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.airsonic.test.cucumber.server;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface AirsonicServer {
|
||||
String getBaseUri();
|
||||
|
||||
void uploadToDefaultMusicFolder(Path directoryPath, String relativePath);
|
||||
|
||||
default void addRestParameters(RequestBuilder builder) {
|
||||
builder.addParameter("c", "inttest");
|
||||
builder.addParameter("v", "1.15.0");
|
||||
builder.addParameter("u", "admin");
|
||||
builder.addParameter("s", "int");
|
||||
builder.addParameter("t", DigestUtils.md5Hex("admin" + "int"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import cucumber.api.java8.En;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.xmlunit.builder.Input;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo;
|
||||
|
||||
public class PingStepDef implements En {
|
||||
|
||||
private CloseableHttpResponse response;
|
||||
private CloseableHttpClient client;
|
||||
|
||||
public PingStepDef(AirsonicServer server) {
|
||||
this.client = HttpClientBuilder.create().build();
|
||||
When("^A ping request is sent$", () -> {
|
||||
HttpGet httpGet = new HttpGet(server.getBaseUri() + "/rest/ping");
|
||||
this.response = client.execute(httpGet);
|
||||
});
|
||||
Then("^A required parameter response is received$", () -> {
|
||||
if(response == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
try {
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
assertEquals(statusLine.getStatusCode(), 200);
|
||||
HttpEntity entity = response.getEntity();
|
||||
String actual = EntityUtils.toString(entity);
|
||||
assertThat(
|
||||
actual,
|
||||
isIdenticalTo(
|
||||
Input.fromStream(
|
||||
getClass().getResourceAsStream("/blobs/ping/missing-auth.xml")))
|
||||
.ignoreWhitespace());
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import cucumber.api.java8.En;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.Assert;
|
||||
import org.subsonic.restapi.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ScanStepDef implements En {
|
||||
|
||||
private final AirsonicServer server;
|
||||
private CloseableHttpResponse response;
|
||||
private CloseableHttpClient client;
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public ScanStepDef(AirsonicServer server) {
|
||||
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
|
||||
this.client = HttpClientBuilder.create().build();
|
||||
this.server = server;
|
||||
|
||||
Given("a scan is done", () -> {
|
||||
Assert.assertFalse(isScanning());
|
||||
|
||||
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/startScan");
|
||||
server.addRestParameters(builder);
|
||||
response = client.execute(builder.build());
|
||||
System.out.println(EntityUtils.toString(response.getEntity()));
|
||||
Long waitTime = 30000L;
|
||||
Long sleepTime = 1000L;
|
||||
while(waitTime > 0 && isScanning()) {
|
||||
waitTime -= sleepTime;
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
|
||||
Assert.assertFalse(isScanning());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private boolean isScanning() throws IOException {
|
||||
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/getScanStatus");
|
||||
builder.addParameter("f", "json");
|
||||
server.addRestParameters(builder);
|
||||
response = client.execute(builder.build());
|
||||
|
||||
String responseAsString = EntityUtils.toString(response.getEntity());
|
||||
JsonNode jsonNode = mapper.readTree(responseAsString).get("subsonic-response");
|
||||
Response response = mapper.treeToValue(jsonNode, Response.class);
|
||||
return response.getScanStatus().isScanning();
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -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.
|
||||
}
|
||||
}
|
||||
+69
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package org.airsonic.test.cucumber_hooks.docker;
|
||||
|
||||
import com.spotify.docker.client.DefaultDockerClient;
|
||||
import com.spotify.docker.client.DockerClient;
|
||||
import com.spotify.docker.client.exceptions.DockerException;
|
||||
import com.spotify.docker.client.messages.*;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.spotify.docker.client.DockerClient.RemoveContainerParam.*;
|
||||
|
||||
@Component
|
||||
@Profile("dynamic")
|
||||
public class DynamicDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean, DisposableBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DynamicDockerHook.class);
|
||||
public static final String AIRSONIC_DOCKER_IMAGE = "airsonic.docker.image";
|
||||
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port";
|
||||
public static final String AIRSONIC_READY_MAX_WAIT = "airsonic.ready.max_wait";
|
||||
public static final String AIRSONIC_READY_SLEEP_TIME = "airsonic.ready.sleep_time";
|
||||
|
||||
private String serverUri = null;
|
||||
private final DockerClient docker;
|
||||
private String containerId;
|
||||
private String dockerImage;
|
||||
private Integer dockerPort;
|
||||
private Long readyMaxWaitTime;
|
||||
private Long readySleepTime;
|
||||
|
||||
public DynamicDockerHook() {
|
||||
logger.debug("Using hook for dynamically creating containers");
|
||||
docker = new DefaultDockerClient("unix:///var/run/docker.sock");
|
||||
testDockerIsAvail();
|
||||
}
|
||||
|
||||
private void testDockerIsAvail() {
|
||||
try {
|
||||
logger.trace("Trying to ping docker daemon");
|
||||
docker.ping();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUri() {
|
||||
if (serverUri == null) {
|
||||
throw new IllegalStateException("Server is not yet ready");
|
||||
} else {
|
||||
return serverUri;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) {
|
||||
try {
|
||||
// TODO ensure localDir is a directory
|
||||
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath);
|
||||
} catch (DockerException | IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void startServer() {
|
||||
logger.debug("Starting server");
|
||||
final ContainerConfig config = ContainerConfig.builder()
|
||||
.image(dockerImage)
|
||||
.build();
|
||||
|
||||
final String name = "airsonic-it-" + RandomStringUtils.randomAlphabetic(10);
|
||||
try {
|
||||
final ContainerCreation containerCreate = docker.createContainer(config, name);
|
||||
containerId = containerCreate.id();
|
||||
docker.startContainer(containerId);
|
||||
Long waitTime = readyMaxWaitTime;
|
||||
while(true) {
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
ContainerState.Health health = containerInfo.state().health();
|
||||
if (health != null && StringUtils.equalsIgnoreCase(health.status(), "healthy")) {
|
||||
logger.trace("Container started early. Yay!");
|
||||
break;
|
||||
} else if(waitTime > readySleepTime) {
|
||||
if(logger.isTraceEnabled()) {
|
||||
String message;
|
||||
if(health != null) {
|
||||
message = "Container ("+name+") not yet ready. State was: " + health.status();
|
||||
} else {
|
||||
message = "Container ("+name+") state unknown. Waiting";
|
||||
}
|
||||
logger.trace(message);
|
||||
}
|
||||
waitTime -= readySleepTime;
|
||||
Thread.sleep(readySleepTime);
|
||||
} else if(health == null) {
|
||||
logger.trace("Max wait time with unknown container state. Hoping container is ready");
|
||||
break;
|
||||
} else {
|
||||
logger.trace("Container ("+name+") never became ready within max wait time");
|
||||
throw new RuntimeException("Container ("+name+") not ready");
|
||||
}
|
||||
}
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
try {
|
||||
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next();
|
||||
String ipAddress = next.getValue().ipAddress();
|
||||
serverUri = "http://" + ipAddress + ":" + dockerPort;
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Could not determine container ("+name+") address", e);
|
||||
}
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopServer() {
|
||||
if(containerId != null) {
|
||||
try {
|
||||
docker.removeContainer(containerId, forceKill(), removeVolumes());
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException("Could not remove container", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
dockerImage = environment.getRequiredProperty(AIRSONIC_DOCKER_IMAGE);
|
||||
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT));
|
||||
readyMaxWaitTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_MAX_WAIT));
|
||||
readySleepTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_SLEEP_TIME));
|
||||
if(readyMaxWaitTime <= 0L || readySleepTime <= 0L) {
|
||||
throw new IllegalArgumentException("Max wait time and sleep time must be greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
stopServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
startServer();
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package org.airsonic.test.cucumber_hooks.docker;
|
||||
|
||||
import com.spotify.docker.client.DefaultDockerClient;
|
||||
import com.spotify.docker.client.DockerClient;
|
||||
import com.spotify.docker.client.exceptions.DockerException;
|
||||
import com.spotify.docker.client.messages.AttachedNetwork;
|
||||
import com.spotify.docker.client.messages.ContainerInfo;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Profile("existing")
|
||||
public class ExistingDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExistingDockerHook.class);
|
||||
public static final String AIRSONIC_DOCKER_CONTAINER = "airsonic.docker.container";
|
||||
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port";
|
||||
|
||||
private String serverUri = null;
|
||||
private final DockerClient docker;
|
||||
private String containerId;
|
||||
private Integer dockerPort;
|
||||
|
||||
public ExistingDockerHook() {
|
||||
logger.debug("Using hook for existing docker container");
|
||||
docker = new DefaultDockerClient("unix:///var/run/docker.sock");
|
||||
testDockerIsAvail();
|
||||
}
|
||||
|
||||
private void testDockerIsAvail() {
|
||||
try {
|
||||
logger.trace("Trying to ping docker daemon");
|
||||
docker.ping();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUri() {
|
||||
if (serverUri == null) {
|
||||
throw new IllegalStateException("Server is not yet ready");
|
||||
} else {
|
||||
return serverUri;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) {
|
||||
try {
|
||||
// TODO ensure localDir is a directory
|
||||
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath);
|
||||
} catch (DockerException | IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void connectToServer() {
|
||||
logger.debug("Connecting to server");
|
||||
|
||||
try {
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
if(!containerInfo.state().running()) {
|
||||
throw new IllegalStateException("Container is not running " + containerId);
|
||||
}
|
||||
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next();
|
||||
String ipAddress = next.getValue().ipAddress();
|
||||
if(StringUtils.isBlank(ipAddress)) {
|
||||
throw new IllegalStateException("No address found for container " + containerId);
|
||||
}
|
||||
serverUri = "http://" + ipAddress + ":" + dockerPort;
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
containerId = environment.getRequiredProperty(AIRSONIC_DOCKER_CONTAINER);
|
||||
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
connectToServer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
airsonic.docker.port=4040
|
||||
airsonic.ready.max_wait=80000
|
||||
airsonic.ready.sleep_time=5000
|
||||
|
||||
# Use for dynamically generating a container as needed
|
||||
spring.profiles.active=dynamic
|
||||
airsonic.docker.image=airsonic/airsonic:${project.version}
|
||||
|
||||
# Use for testing against an existing/running container
|
||||
#airsonic.docker.container=1212be8a94e0
|
||||
#spring.profiles.active=existing
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="failed" version="1.15.0">
|
||||
<error code="10" message="Required parameter is missing."/>
|
||||
</subsonic-response>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.15.0"/>
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
Feature: Ping API
|
||||
|
||||
Scenario: Airsonic responds to ping requests
|
||||
When A ping request is sent
|
||||
Then A required parameter response is received
|
||||
@@ -0,0 +1,11 @@
|
||||
Feature: Stream API for MP3
|
||||
|
||||
Background:
|
||||
Given Media file stream/piano/piano.mp3 is added
|
||||
And a scan is done
|
||||
|
||||
Scenario: Airsonic sends stream data
|
||||
When A stream request is sent
|
||||
Then The response bytes are equal
|
||||
# TODO check length
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger level="trace" name="org.airsonic.test.cucumber_hooks.docker.DockerHook" />
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user