diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java index 39aedfb4..e7683215 100644 --- a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java @@ -2,9 +2,12 @@ package org.airsonic.test.cucumber.steps.api; import cucumber.api.java8.En; import org.airsonic.test.cucumber.server.AirsonicServer; +import org.airsonic.test.domain.SavedHttpResponse; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.HexDump; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HeaderElement; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.impl.client.CloseableHttpClient; @@ -14,56 +17,117 @@ import org.junit.Assert; import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class StreamStepDef implements En { - private CloseableHttpResponse response; private CloseableHttpClient client; - private boolean closed = false; - private byte[] body; + private List responses = new ArrayList<>(); + private String streamName; public StreamStepDef(AirsonicServer server) { this.client = HttpClientBuilder.create().build(); - Given("Media file (.*) is added", (String mediaFile) -> { - // TODO fix this + Given("Media file (.*) is added", (String streamName) -> { + this.streamName = streamName; server.uploadToDefaultMusicFolder( - Paths.get(this.getClass().getResource("/blobs/stream/piano").toURI()), + Paths.get(this.getClass().getResource("/blobs/stream/" + streamName).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(); + for (int i = 0; i < responses.size(); i++) { + checkBody(responses.get(i), i); + } + }); + Then("^Print debug output$", () -> { + for (int i = 0; i < responses.size(); i++) { + System.out.printf("Response %d%n", i + 1); + printDebugInfo(responses.get(i), 2); + } + }); + Then("^The length headers are correct$", () -> { + responses.forEach(this::checkLengths); + }); + When("^A stream is consumed$", () -> { + while(shouldDoRequest()) { + responses.add(consumeResponse(doRequest(server))); + } + }); - FileUtils.writeByteArrayToFile(new File("/tmp/bytearray"), body); + } - byte[] expected = IOUtils.toByteArray(this.getClass().getResource("/blobs/stream/piano/piano.mp3").toURI()); -// + private void checkBody(SavedHttpResponse savedHttpResponse, int iter) throws URISyntaxException, IOException { + String expectedBodyResource = String.format("/blobs/stream/"+streamName+"/responses/%d.dat", iter+1); + byte[] expected = IOUtils.toByteArray( + this.getClass() + .getResource(expectedBodyResource) + .toURI()); + // TODO if debug... +// FileUtils.writeByteArrayToFile( +// new File(String.format("/tmp/bytearray-%d", iter+1)), +// savedHttpResponse.getBody()); // HexDump.dump(expected, 0, System.out, 0); - Assert.assertArrayEquals(expected, body); - }); + Assert.assertArrayEquals(expected, savedHttpResponse.getBody()); + + } + private void printDebugInfo(SavedHttpResponse savedHttpResponse, int indentLevel) { + String indent = StringUtils.repeat(' ', indentLevel); + System.out.println(indent + "Headers:"); + for (Header header : savedHttpResponse.getHeaders()) { + System.out.print(indent + header.getName()); + System.out.print(": "); + for (HeaderElement element : header.getElements()) { + System.out.print(element); + System.out.print(", "); + } + System.out.println(); + } + } + + private void checkLengths(SavedHttpResponse response) { + Header header = response.getHeader("Content-Length"); + Assert.assertEquals(response.getBody().length, Integer.parseInt(header.getValue())); + } + private boolean shouldDoRequest() { + return responses.isEmpty() || isUnconsumedContent(responses.get(responses.size() - 1)); } - void ensureBodyRead() throws IOException { - if(closed) { - return; - } else { - this.body = EntityUtils.toByteArray(response.getEntity()); - closed = true; - response.close(); + private Pattern CONTENT_RANGE_PATTERN = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); + private boolean isUnconsumedContent(SavedHttpResponse savedHttpResponse) { + Header header = savedHttpResponse.getHeader("Content-Range"); + Matcher matcher = CONTENT_RANGE_PATTERN.matcher(header.getValue()); + if(!matcher.matches()) { + throw new RuntimeException("Unexpected Content-Range format"); } + int start = Integer.parseInt(matcher.group(1)); + int end = Integer.parseInt(matcher.group(2)); + int total = Integer.parseInt(matcher.group(3)); + return (total - 1) > end; + } + + private CloseableHttpResponse doRequest(AirsonicServer server) throws IOException { + RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/stream"); + builder.addParameter("id", "2"); // TODO abstract this out + builder.addHeader("Range", "bytes=0-"); + builder.addHeader("Accept", "audio/webm,audio/ogg,audio/wav,audio/*;"); + server.addRestParameters(builder); + return client.execute(builder.build()); + } + + SavedHttpResponse consumeResponse(CloseableHttpResponse response) throws IOException { + byte[] body = EntityUtils.toByteArray(response.getEntity()); + List
headers = Arrays.asList(response.getAllHeaders()); + response.close(); + return new SavedHttpResponse(headers, body); } } diff --git a/integration-test/src/test/java/org/airsonic/test/domain/SavedHttpResponse.java b/integration-test/src/test/java/org/airsonic/test/domain/SavedHttpResponse.java new file mode 100644 index 00000000..8fe66551 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/domain/SavedHttpResponse.java @@ -0,0 +1,36 @@ +package org.airsonic.test.domain; + +import org.apache.http.Header; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class SavedHttpResponse { + private final List
headers; + private final byte[] body; + + public SavedHttpResponse(List
headers, byte[] body) { + this.headers = headers; + this.body = body; + } + + public List
getHeaders() { + return headers; + } + + public byte[] getBody() { + return body; + } + + public Header getHeader(String name) { + List
matchingHeaders = headers.stream().filter(header -> + Objects.equals(header.getName(), name)) + .collect(Collectors.toList()); + if(matchingHeaders.size() != 1) { + throw new RuntimeException("Did not find one matching header with name " + name); + } + return matchingHeaders.iterator().next(); + } + +} diff --git a/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 b/integration-test/src/test/resources/blobs/stream/piano/input/piano.mp3 similarity index 100% rename from integration-test/src/test/resources/blobs/stream/piano/piano.mp3 rename to integration-test/src/test/resources/blobs/stream/piano/input/piano.mp3 diff --git a/integration-test/src/test/resources/blobs/stream/piano/responses/1.dat b/integration-test/src/test/resources/blobs/stream/piano/responses/1.dat new file mode 100644 index 00000000..dd253d5d Binary files /dev/null and b/integration-test/src/test/resources/blobs/stream/piano/responses/1.dat differ diff --git a/integration-test/src/test/resources/features/api/stream-mp3.feature b/integration-test/src/test/resources/features/api/stream-mp3.feature index 86fa4358..977ccb49 100644 --- a/integration-test/src/test/resources/features/api/stream-mp3.feature +++ b/integration-test/src/test/resources/features/api/stream-mp3.feature @@ -1,11 +1,12 @@ Feature: Stream API for MP3 Background: - Given Media file stream/piano/piano.mp3 is added + Given Media file piano is added And a scan is done Scenario: Airsonic sends stream data - When A stream request is sent + When A stream is consumed + Then Print debug output Then The response bytes are equal - # TODO check length + Then The length headers are correct