|  |  |  | @ -1,5 +1,8 @@ | 
			
		
	
		
			
				
					|  |  |  |  | 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.airsonic.test.domain.SavedHttpResponse; | 
			
		
	
	
		
			
				
					|  |  |  | @ -14,6 +17,9 @@ 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.Child; | 
			
		
	
		
			
				
					|  |  |  |  | import org.subsonic.restapi.MusicFolder; | 
			
		
	
		
			
				
					|  |  |  |  | import org.subsonic.restapi.Response; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | import java.io.File; | 
			
		
	
		
			
				
					|  |  |  |  | import java.io.IOException; | 
			
		
	
	
		
			
				
					|  |  |  | @ -22,6 +28,7 @@ import java.nio.file.Paths; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.ArrayList; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.Arrays; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.List; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.Objects; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.regex.Matcher; | 
			
		
	
		
			
				
					|  |  |  |  | import java.util.regex.Pattern; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -30,13 +37,16 @@ public class StreamStepDef implements En { | 
			
		
	
		
			
				
					|  |  |  |  |     private CloseableHttpClient client; | 
			
		
	
		
			
				
					|  |  |  |  |     private List<SavedHttpResponse> responses = new ArrayList<>(); | 
			
		
	
		
			
				
					|  |  |  |  |     private String streamName; | 
			
		
	
		
			
				
					|  |  |  |  |     private String mediaFileId; | 
			
		
	
		
			
				
					|  |  |  |  |     private ObjectMapper mapper = new ObjectMapper(); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     public StreamStepDef(AirsonicServer server) { | 
			
		
	
		
			
				
					|  |  |  |  |         mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); | 
			
		
	
		
			
				
					|  |  |  |  |         this.client = HttpClientBuilder.create().build(); | 
			
		
	
		
			
				
					|  |  |  |  |         Given("Media file (.*) is added", (String streamName) -> { | 
			
		
	
		
			
				
					|  |  |  |  |             this.streamName = streamName; | 
			
		
	
		
			
				
					|  |  |  |  |             server.uploadToDefaultMusicFolder( | 
			
		
	
		
			
				
					|  |  |  |  |                     Paths.get(this.getClass().getResource("/blobs/stream/" + streamName).toURI()), | 
			
		
	
		
			
				
					|  |  |  |  |                     Paths.get(this.getClass().getResource("/blobs/stream/" + streamName + "/input").toURI()), | 
			
		
	
		
			
				
					|  |  |  |  |                     ""); | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -54,25 +64,70 @@ public class StreamStepDef implements En { | 
			
		
	
		
			
				
					|  |  |  |  |         Then("^The length headers are correct$", () -> { | 
			
		
	
		
			
				
					|  |  |  |  |             responses.forEach(this::checkLengths); | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  |         Then("^The length headers are absent$", () -> { | 
			
		
	
		
			
				
					|  |  |  |  |             responses.forEach(this::noLengths); | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  |         When("^A stream is consumed$", () -> { | 
			
		
	
		
			
				
					|  |  |  |  |             while(shouldDoRequest()) { | 
			
		
	
		
			
				
					|  |  |  |  |                 responses.add(consumeResponse(doRequest(server))); | 
			
		
	
		
			
				
					|  |  |  |  |             } | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  |         Then("^Save response bodies to files$", () -> { | 
			
		
	
		
			
				
					|  |  |  |  |             for (int i = 0; i < responses.size(); i++) { | 
			
		
	
		
			
				
					|  |  |  |  |                 saveBody(responses.get(i), i); | 
			
		
	
		
			
				
					|  |  |  |  |             } | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  |         And("^The media file id is found$", () -> { | 
			
		
	
		
			
				
					|  |  |  |  |             mediaFileId = getMediaFilesInMusicFolder(server).get(0).getId(); | 
			
		
	
		
			
				
					|  |  |  |  |         }); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private List<Child> getMediaFilesInMusicFolder(AirsonicServer server) throws IOException { | 
			
		
	
		
			
				
					|  |  |  |  |         RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/getMusicFolders"); | 
			
		
	
		
			
				
					|  |  |  |  |         builder.addParameter("f", "json"); | 
			
		
	
		
			
				
					|  |  |  |  |         server.addRestParameters(builder); | 
			
		
	
		
			
				
					|  |  |  |  |         CloseableHttpResponse response = client.execute(builder.build()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         String responseAsString = EntityUtils.toString(response.getEntity()); | 
			
		
	
		
			
				
					|  |  |  |  |         JsonNode jsonNode = mapper.readTree(responseAsString).get("subsonic-response"); | 
			
		
	
		
			
				
					|  |  |  |  |         Response subsonicResponse = mapper.treeToValue(jsonNode, Response.class); | 
			
		
	
		
			
				
					|  |  |  |  |         List<MusicFolder> musicFolder = subsonicResponse.getMusicFolders().getMusicFolder(); | 
			
		
	
		
			
				
					|  |  |  |  |         MusicFolder music = musicFolder | 
			
		
	
		
			
				
					|  |  |  |  |                 .stream() | 
			
		
	
		
			
				
					|  |  |  |  |                 .filter(folder -> Objects.equals(folder.getName(), "Music")) | 
			
		
	
		
			
				
					|  |  |  |  |                 .findFirst() | 
			
		
	
		
			
				
					|  |  |  |  |                 .orElseThrow(() -> new RuntimeException("No top level folder named Music")); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         return getMediaFiles(server, music.getId()); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private List<Child> getMediaFiles(AirsonicServer server, int folderId) throws IOException { | 
			
		
	
		
			
				
					|  |  |  |  |         RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/getIndexes"); | 
			
		
	
		
			
				
					|  |  |  |  |         builder.addParameter("f", "json"); | 
			
		
	
		
			
				
					|  |  |  |  |         builder.addParameter("musicFolderId", String.valueOf(folderId)); | 
			
		
	
		
			
				
					|  |  |  |  |         server.addRestParameters(builder); | 
			
		
	
		
			
				
					|  |  |  |  |         CloseableHttpResponse response = client.execute(builder.build()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         String responseAsString = EntityUtils.toString(response.getEntity()); | 
			
		
	
		
			
				
					|  |  |  |  |         JsonNode jsonNode = mapper.readTree(responseAsString).get("subsonic-response"); | 
			
		
	
		
			
				
					|  |  |  |  |         Response subsonicResponse = mapper.treeToValue(jsonNode, Response.class); | 
			
		
	
		
			
				
					|  |  |  |  |         return subsonicResponse.getIndexes().getChild(); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private void saveBody(SavedHttpResponse savedHttpResponse, int iter) throws IOException { | 
			
		
	
		
			
				
					|  |  |  |  |         FileUtils.writeByteArrayToFile( | 
			
		
	
		
			
				
					|  |  |  |  |                 new File(String.format("/tmp/bytearray-%d", iter+1)), | 
			
		
	
		
			
				
					|  |  |  |  |                 savedHttpResponse.getBody()); | 
			
		
	
		
			
				
					|  |  |  |  |         // TODO if debug...
 | 
			
		
	
		
			
				
					|  |  |  |  | //            HexDump.dump(expected, 0, System.out, 0);
 | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     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);
 | 
			
		
	
		
			
				
					|  |  |  |  |                     .getResourceAsStream(expectedBodyResource)); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         Assert.assertArrayEquals(expected, savedHttpResponse.getBody()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -97,13 +152,29 @@ public class StreamStepDef implements En { | 
			
		
	
		
			
				
					|  |  |  |  |         Assert.assertEquals(response.getBody().length, Integer.parseInt(header.getValue())); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private void noLengths(SavedHttpResponse response) { | 
			
		
	
		
			
				
					|  |  |  |  |         Header header = response.getHeader("Content-Length"); | 
			
		
	
		
			
				
					|  |  |  |  |         Assert.assertNull(header); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private boolean shouldDoRequest() { | 
			
		
	
		
			
				
					|  |  |  |  |         return responses.isEmpty() || isUnconsumedContent(responses.get(responses.size() - 1)); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private Pattern CONTENT_RANGE_PATTERN = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); | 
			
		
	
		
			
				
					|  |  |  |  |     private boolean isUnconsumedContent(SavedHttpResponse savedHttpResponse) { | 
			
		
	
		
			
				
					|  |  |  |  |         ContentRange contentRange = getContentRange(savedHttpResponse); | 
			
		
	
		
			
				
					|  |  |  |  |         if(contentRange == null) { | 
			
		
	
		
			
				
					|  |  |  |  |             return false; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |         return (contentRange.getTotal() - 1) > contentRange.getEnd(); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private ContentRange getContentRange(SavedHttpResponse savedHttpResponse) { | 
			
		
	
		
			
				
					|  |  |  |  |         Header header = savedHttpResponse.getHeader("Content-Range"); | 
			
		
	
		
			
				
					|  |  |  |  |         if(header == null) { | 
			
		
	
		
			
				
					|  |  |  |  |             return null; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |         Matcher matcher = CONTENT_RANGE_PATTERN.matcher(header.getValue()); | 
			
		
	
		
			
				
					|  |  |  |  |         if(!matcher.matches()) { | 
			
		
	
		
			
				
					|  |  |  |  |             throw new RuntimeException("Unexpected Content-Range format"); | 
			
		
	
	
		
			
				
					|  |  |  | @ -111,23 +182,62 @@ public class StreamStepDef implements En { | 
			
		
	
		
			
				
					|  |  |  |  |         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; | 
			
		
	
		
			
				
					|  |  |  |  |         return new ContentRange(start, end, total); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private String calculateRange() { | 
			
		
	
		
			
				
					|  |  |  |  |         Integer start = null; | 
			
		
	
		
			
				
					|  |  |  |  |         Integer end = null; | 
			
		
	
		
			
				
					|  |  |  |  |         if(responses.isEmpty()) { | 
			
		
	
		
			
				
					|  |  |  |  |             start = 0; | 
			
		
	
		
			
				
					|  |  |  |  |         } else { | 
			
		
	
		
			
				
					|  |  |  |  |             SavedHttpResponse lastResponse = responses.get(responses.size() - 1); | 
			
		
	
		
			
				
					|  |  |  |  |             ContentRange contentRange = getContentRange(lastResponse); | 
			
		
	
		
			
				
					|  |  |  |  |             start = contentRange.getEnd(); | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |         return start + "-" + (end == null ? "" : 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.addParameter("id", mediaFileId); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         String range = calculateRange(); | 
			
		
	
		
			
				
					|  |  |  |  |         System.out.println("In request "+ (responses.size() + 1) +" asking for range " + range); | 
			
		
	
		
			
				
					|  |  |  |  |         builder.addHeader("Range", "bytes=" + range); | 
			
		
	
		
			
				
					|  |  |  |  |         builder.addHeader("Accept", "audio/webm,audio/ogg,audio/wav,audio/*;"); | 
			
		
	
		
			
				
					|  |  |  |  |         server.addRestParameters(builder); | 
			
		
	
		
			
				
					|  |  |  |  |         return client.execute(builder.build()); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     SavedHttpResponse consumeResponse(CloseableHttpResponse response) throws IOException { | 
			
		
	
		
			
				
					|  |  |  |  |     private SavedHttpResponse consumeResponse(CloseableHttpResponse response) throws IOException { | 
			
		
	
		
			
				
					|  |  |  |  |         byte[] body = EntityUtils.toByteArray(response.getEntity()); | 
			
		
	
		
			
				
					|  |  |  |  |         List<Header> headers = Arrays.asList(response.getAllHeaders()); | 
			
		
	
		
			
				
					|  |  |  |  |         response.close(); | 
			
		
	
		
			
				
					|  |  |  |  |         return new SavedHttpResponse(headers, body); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     private class ContentRange { | 
			
		
	
		
			
				
					|  |  |  |  |         private final int start; | 
			
		
	
		
			
				
					|  |  |  |  |         private final int end; | 
			
		
	
		
			
				
					|  |  |  |  |         private final int total; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         public ContentRange(int start, int end, int total) { | 
			
		
	
		
			
				
					|  |  |  |  |             this.start = start; | 
			
		
	
		
			
				
					|  |  |  |  |             this.end = end; | 
			
		
	
		
			
				
					|  |  |  |  |             this.total = total; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         public int getStart() { | 
			
		
	
		
			
				
					|  |  |  |  |             return start; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         public int getEnd() { | 
			
		
	
		
			
				
					|  |  |  |  |             return end; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         public int getTotal() { | 
			
		
	
		
			
				
					|  |  |  |  |             return total; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
	
		
			
				
					|  |  |  | 
 |