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