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