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