diff --git a/pom.xml b/pom.xml index bfea82a..305760b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ engineer.nightowl sonos-api-java - 0.0.27-SNAPSHOT + 0.2.0-SNAPSHOT jar sonos-api-java @@ -70,11 +70,6 @@ commons-lang3 3.20.0 - - org.apache.httpcomponents - httpclient - 4.5.14 - com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java b/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java index fcccbcb..aa812ce 100644 --- a/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java +++ b/src/main/java/engineer/nightowl/sonos/api/SonosApiClient.java @@ -15,13 +15,11 @@ import engineer.nightowl.sonos.api.resource.PlaylistResource; import engineer.nightowl.sonos.api.resource.SettingsResource; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.VersionInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.http.HttpClient; import java.util.Properties; public class SonosApiClient implements AutoCloseable @@ -48,7 +46,7 @@ public class SonosApiClient implements AutoCloseable // Can be overridden by implementing applications private SonosApiConfiguration configuration; - private CloseableHttpClient httpClient; + private HttpClient httpClient; private final String version; @@ -58,7 +56,7 @@ public class SonosApiClient implements AutoCloseable * @param configuration a {@link engineer.nightowl.sonos.api.SonosApiConfiguration} containing integration * information such as API keys * - * @see SonosApiClient#SonosApiClient(SonosApiConfiguration, CloseableHttpClient) + * @see SonosApiClient#SonosApiClient(SonosApiConfiguration, HttpClient) */ public SonosApiClient(final SonosApiConfiguration configuration) { @@ -70,9 +68,9 @@ public SonosApiClient(final SonosApiConfiguration configuration) * * @param configuration a {@link engineer.nightowl.sonos.api.SonosApiConfiguration} containing integration * information such as API keys - * @param httpClient a custom {@link CloseableHttpClient} - if null, a default client is initialised + * @param httpClient a custom {@link HttpClient} - if null, a default client is initialised */ - public SonosApiClient(final SonosApiConfiguration configuration, final CloseableHttpClient httpClient) + public SonosApiClient(final SonosApiConfiguration configuration, final HttpClient httpClient) { loadProperties(); version = properties.getProperty("sonosapijava.version"); @@ -101,11 +99,8 @@ public SonosApiClient(final SonosApiConfiguration configuration, final Closeable public String getUserAgent() { - final String ahcUa = VersionInfo.getUserAgent("Apache-HttpClient", - "org.apache.http.client", HttpClientBuilder.class); - - return String.format("sonos-api-java/%s (applicationId/%s) (httpClient/(%s))", - version, configuration.getApplicationId(), ahcUa); + return String.format("sonos-api-java/%s (applicationId/%s) (Java/%s)", + version, configuration.getApplicationId(), System.getProperty("java.version")); } /** @@ -113,10 +108,12 @@ public String getUserAgent() * * @return a default HTTP client. */ - private CloseableHttpClient generateHttpClient() + private HttpClient generateHttpClient() { logger.debug("Using default HttpClient"); - return HttpClientBuilder.create().setUserAgent(getUserAgent()).build(); + return HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); } /** @@ -124,13 +121,7 @@ private CloseableHttpClient generateHttpClient() */ public void closeHttpClient() { - try - { - httpClient.close(); - } catch (final IOException ioe) - { - logger.warn("Unable to close HttpClient", ioe); - } + httpClient.close(); } /** @@ -138,7 +129,7 @@ public void closeHttpClient() * * @return the configured HTTP client */ - public CloseableHttpClient getHttpClient() + public HttpClient getHttpClient() { return httpClient; } @@ -148,7 +139,7 @@ public CloseableHttpClient getHttpClient() * * @param httpClient custom client to set */ - public void setHttpClient(final CloseableHttpClient httpClient) + public void setHttpClient(final HttpClient httpClient) { this.httpClient = httpClient; } diff --git a/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java b/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java index 69640af..71233c7 100644 --- a/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java +++ b/src/main/java/engineer/nightowl/sonos/api/SonosApiConfiguration.java @@ -2,9 +2,8 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.http.Header; -import org.apache.http.message.BasicHeader; +import java.nio.charset.StandardCharsets; import java.util.Base64; /** @@ -136,14 +135,13 @@ public void loadDefaults() /** *

getAuthorizationHeader.

* - * @return a {@link org.apache.http.Header} object. + * @return the value to use for the {@code Authorization} header - e.g. {@code "Basic "}. */ - public Header getAuthorizationHeader() + public String getAuthorizationHeader() { - final byte[] authBytes = String.join(":", getApiKey(), getApiSecret()).getBytes(); + final byte[] authBytes = String.join(":", getApiKey(), getApiSecret()).getBytes(StandardCharsets.UTF_8); final String authBase64 = Base64.getEncoder().encodeToString(authBytes); - final String headerValue = String.join(" ", "Basic", authBase64); - return new BasicHeader("Authorization", headerValue); + return String.join(" ", "Basic", authBase64); } @Override diff --git a/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java b/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java index 917528d..aa9965f 100644 --- a/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java +++ b/src/main/java/engineer/nightowl/sonos/api/resource/AuthorizeResource.java @@ -5,18 +5,17 @@ import engineer.nightowl.sonos.api.domain.SonosToken; import engineer.nightowl.sonos.api.exception.SonosApiClientException; import engineer.nightowl.sonos.api.exception.SonosApiError; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.message.BasicNameValuePair; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.URI; -import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * The Authorization flow is dependent on sending your user back to a pre-registered, and user-accessible @@ -52,25 +51,22 @@ public AuthorizeResource(final SonosApiClient apiClient) public URI getAuthorizeCodeUri(final String redirectUri, final String state) throws SonosApiClientException { final SonosApiConfiguration configuration = apiClient.getConfiguration(); - final URIBuilder uri = new URIBuilder(); - uri.setScheme(HTTPS); - uri.setHost(configuration.getAuthBaseUrl()); - uri.setPath("/login/v3/oauth"); - uri.setParameter("client_id", configuration.getApiKey()); - uri.setParameter("redirect_uri", redirectUri); + final List> params = new ArrayList<>(); + params.add(Map.entry("client_id", configuration.getApiKey())); + params.add(Map.entry("redirect_uri", redirectUri)); // Only currently supported values by Sonos - uri.setParameter("response_type", "code"); - uri.setParameter("scope", "playback-control-all"); + params.add(Map.entry("response_type", "code")); + params.add(Map.entry("scope", "playback-control-all")); // State is optional, only include it if set. if (state != null) { - uri.setParameter("state", state); + params.add(Map.entry("state", state)); } try { - return uri.build(); - } catch (final URISyntaxException e) + return URI.create(String.format("%s://%s/login/v3/oauth?%s", HTTPS, configuration.getAuthBaseUrl(), encodeParameters(params))); + } catch (final IllegalArgumentException e) { throw new SonosApiClientException("Invalid URI built", e); } @@ -102,10 +98,10 @@ public URI getAuthorizeCodeUri(final String redirectUri) throws SonosApiClientEx public SonosToken createToken(final String redirectUri, final String authorizeCode) throws SonosApiClientException, SonosApiError { - final List postParameters = new ArrayList<>(); - postParameters.add(new BasicNameValuePair("redirect_uri", redirectUri)); - postParameters.add(new BasicNameValuePair("code", authorizeCode)); - postParameters.add(new BasicNameValuePair("grant_type", "authorization_code")); + final List> postParameters = new ArrayList<>(); + postParameters.add(Map.entry("redirect_uri", redirectUri)); + postParameters.add(Map.entry("code", authorizeCode)); + postParameters.add(Map.entry("grant_type", "authorization_code")); return callApi(buildOauthAccessRequest(postParameters), SonosToken.class); } @@ -121,9 +117,9 @@ public SonosToken createToken(final String redirectUri, final String authorizeCo */ public SonosToken refreshToken(final String refreshToken) throws SonosApiClientException, SonosApiError { - final List postParameters = new ArrayList<>(); - postParameters.add(new BasicNameValuePair("refresh_token", refreshToken)); - postParameters.add(new BasicNameValuePair("grant_type", "refresh_token")); + final List> postParameters = new ArrayList<>(); + postParameters.add(Map.entry("refresh_token", refreshToken)); + postParameters.add(Map.entry("grant_type", "refresh_token")); return callApi(buildOauthAccessRequest(postParameters), SonosToken.class); } @@ -136,33 +132,42 @@ public SonosToken refreshToken(final String refreshToken) throws SonosApiClientE * @return the built request, ready to execute via {@link #callApi} * @throws SonosApiClientException if the request could not be built */ - private HttpPost buildOauthAccessRequest(final List postParameters) throws SonosApiClientException + private HttpRequest buildOauthAccessRequest(final List> postParameters) throws SonosApiClientException { final SonosApiConfiguration configuration = apiClient.getConfiguration(); - final URIBuilder uri = new URIBuilder(); - uri.setScheme(HTTPS); - uri.setHost(configuration.getAuthBaseUrl()); - uri.setPath("/login/v3/oauth/access"); - - final HttpPost request = new HttpPost(); - try - { - request.setEntity(new UrlEncodedFormEntity(postParameters)); - } catch (final UnsupportedEncodingException e) - { - throw new SonosApiClientException("Unable to generate auth request content", e); - } - request.setHeader(configuration.getAuthorizationHeader()); - + final URI uri; try { - request.setURI(uri.build()); - } catch (final URISyntaxException e) + uri = URI.create(String.format("%s://%s/login/v3/oauth/access", HTTPS, configuration.getAuthBaseUrl())); + } catch (final IllegalArgumentException e) { throw new SonosApiClientException("Invalid URI built", e); } - return request; + return HttpRequest.newBuilder(uri) + .setHeader("Authorization", configuration.getAuthorizationHeader()) + .setHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .setHeader("User-Agent", apiClient.getUserAgent()) + .POST(HttpRequest.BodyPublishers.ofString(encodeParameters(postParameters), StandardCharsets.UTF_8)) + .build(); + } + + /** + * URL-encode and join a list of form/query parameters as {@code key=value&key2=value2}. + * + * @param parameters the parameters to encode, in order + * @return the encoded, joined parameter string + */ + private static String encodeParameters(final List> parameters) + { + return parameters.stream() + .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue())) + .collect(Collectors.joining("&")); + } + + private static String encode(final String value) + { + return URLEncoder.encode(value, StandardCharsets.UTF_8); } /** diff --git a/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java b/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java index 999f4bb..b6572a9 100644 --- a/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java +++ b/src/main/java/engineer/nightowl/sonos/api/resource/BaseResource.java @@ -11,22 +11,15 @@ import engineer.nightowl.sonos.api.exception.SonosApiError; import engineer.nightowl.sonos.api.specs.Validatable; import engineer.nightowl.sonos.api.util.SonosUtilityHelper; -import org.apache.http.Header; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Optional; /** * Generic base class used for integrating with the Sonos API. @@ -54,7 +47,7 @@ class BaseResource } /** - * Main API call method. Takes in a {@link HttpUriRequest} comprising of a + * Main API call method. Takes in a {@link HttpRequest} comprising of a * URI and method * * @param request with the relevant URI and method (with associated data as @@ -64,85 +57,79 @@ class BaseResource * @throws SonosApiClientException if unable to build or execute the request * @throws SonosApiError if the Sonos API returns an error to an otherwise successful request */ - T callApi(final HttpUriRequest request, final Class type) throws SonosApiClientException, SonosApiError + T callApi(final HttpRequest request, final Class type) throws SonosApiClientException, SonosApiError { - logger.debug("Sending request to {}", request.getURI()); - try (final CloseableHttpResponse response = executeRequest(request)) + logger.debug("Sending request to {}", request.uri()); + final HttpResponse response = executeRequest(request); + + if (401 == response.statusCode()) + { + throw new SonosApiClientException("Invalid token"); + } + final byte[] bytes = response.body(); + + logger.debug("Raw response from API: {}", response); + logger.debug("Raw response content from API: {}", bytes); + + // Get type from Sonos response - not always possible + final SonosType sonosDeclaredClass = getTypeFromHeader(response); + final String sonosDeclaredClassName = sonosDeclaredClass == null ? null : sonosDeclaredClass.getClazz().getSimpleName(); + + // If Sonos didn't provide a type, or if one was provided and it matches what we wanted returned, proceed + if (sonosDeclaredClassName == null || sonosDeclaredClassName.equals(type.getSimpleName())) { - if (401 == response.getStatusLine().getStatusCode()) + try + { + return OM.readValue(bytes, type); + } catch (final IOException e) { - throw new SonosApiClientException("Invalid token"); + final String msg = String.format("Unexpected error converting response to %s (Sonos declared %s)", type.getSimpleName(), sonosDeclaredClassName); + throw new SonosApiClientException(msg, e); } - final byte[] bytes; - try (final InputStream stream = response.getEntity().getContent()) + } + // Otherwise it's not what we expected - likely an error object, in which case throw an exception with the mapped object + else + { + final Object responseContent; + try { - bytes = stream.readAllBytes(); - } catch (final IOException ioe) + responseContent = OM.readValue(bytes, sonosDeclaredClass.getClazz()); + } catch (final IOException e) { - throw new SonosApiClientException("Unable to convert response body", ioe); + throw new SonosApiClientException("Unable to parse error response from Sonos", e); } - logger.debug("Raw response from API: {}", response); - logger.debug("Raw response content from API: {}", bytes); - - // Get type from Sonos response - not always possible - final SonosType sonosDeclaredClass = getTypeFromHeader(response); - final String sonosDeclaredClassName = sonosDeclaredClass == null ? null : sonosDeclaredClass.getClazz().getSimpleName(); - - // If Sonos didn't provide a type, or if one was provided and it matches what we wanted returned, proceed - if (sonosDeclaredClassName == null || sonosDeclaredClassName.equals(type.getSimpleName())) + if (SonosType.getErrorTypes().contains(sonosDeclaredClass)) { - try - { - return OM.readValue(bytes, type); - } catch (final IOException e) - { - final String msg = String.format("Unexpected error converting response to %s (Sonos declared %s)", type.getSimpleName(), sonosDeclaredClassName); - throw new SonosApiClientException(msg, e); - } - } - // Otherwise it's not what we expected - likely an error object, in which case throw an exception with the mapped object - else + throw (SonosApiError) sonosDeclaredClass.getClazz().cast(responseContent); + } else { - final Object responseContent; - try - { - responseContent = OM.readValue(bytes, sonosDeclaredClass.getClazz()); - } catch (final IOException e) - { - throw new SonosApiClientException("Unable to parse error response from Sonos", e); - } - - if (SonosType.getErrorTypes().contains(sonosDeclaredClass)) - { - throw (SonosApiError) sonosDeclaredClass.getClazz().cast(responseContent); - } else - { - final String mismatchMsg = String.format("Sonos declared %s as the response type, but the integration requested %s", sonosDeclaredClassName, type.getSimpleName()); - throw new SonosApiClientException(mismatchMsg); - } + final String mismatchMsg = String.format("Sonos declared %s as the response type, but the integration requested %s", sonosDeclaredClassName, type.getSimpleName()); + throw new SonosApiClientException(mismatchMsg); } - } catch (final IOException ioe) - { - throw new SonosApiClientException("Error closing response from Sonos API", ioe); } } /** - * Execute a request, wrapping transport failures in a {@link SonosApiClientException}. + * Execute a request, wrapping transport failures in a {@link SonosApiClientException}. The response + * body is fully buffered before this method returns, so there is nothing for callers to close. * * @param request the request to execute - * @return the raw response - callers are responsible for closing it + * @return the response * @throws SonosApiClientException if the request could not be executed */ - private CloseableHttpResponse executeRequest(final HttpUriRequest request) throws SonosApiClientException + private HttpResponse executeRequest(final HttpRequest request) throws SonosApiClientException { try { - return apiClient.getHttpClient().execute(request); + return apiClient.getHttpClient().send(request, HttpResponse.BodyHandlers.ofByteArray()); } catch (final IOException e) { throw new SonosApiClientException("Error interrogating Sonos API", e); + } catch (final InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new SonosApiClientException("Interrupted while calling Sonos API", e); } } @@ -152,14 +139,14 @@ private CloseableHttpResponse executeRequest(final HttpUriRequest request) throw * @param response - raw response to fetch the header from * @return the {@link SonosType} declared, or null if not found */ - SonosType getTypeFromHeader(final CloseableHttpResponse response) throws SonosApiClientException + SonosType getTypeFromHeader(final HttpResponse response) throws SonosApiClientException { if (response != null) { - final Header header = response.getFirstHeader(SONOS_TYPE_HEADER); - if (header != null && !SonosUtilityHelper.isEmpty(header.getValue())) + final Optional header = response.headers().firstValue(SONOS_TYPE_HEADER); + if (header.isPresent() && !SonosUtilityHelper.isEmpty(header.get())) { - final String headerValue = header.getValue(); + final String headerValue = header.get(); if (!"none".equalsIgnoreCase(headerValue)) { try @@ -190,7 +177,7 @@ SonosType getTypeFromHeader(final CloseableHttpResponse response) throws SonosAp */ T getFromApi(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError { - final HttpGet request = getGetRequest(token, path); + final HttpRequest request = getGetRequest(token, path); return callApi(request, returnType); } @@ -207,7 +194,7 @@ T getFromApi(final Class returnType, final String token, final String pat */ T deleteFromApi(final Class returnType, final String token, final String path) throws SonosApiClientException, SonosApiError { - final HttpDelete request = getDeleteRequest(token, path); + final HttpRequest request = getDeleteRequest(token, path); return callApi(request, returnType); } @@ -243,7 +230,7 @@ T postToApi(final Class returnType, final String token, final String path T postToApi(final Class returnType, final String token, final String path, final U content) throws SonosApiClientException, SonosApiError { - final HttpPost request = getPostRequest(token, path); + final HttpRequest.Builder builder = getStandardRequest(token, path); final Boolean validationEnabled = apiClient.getConfiguration().isClientSideValidationEnabled(); // If the content for the request has the ability to be validated, do so if enabled. // If the object is invalid, there's no point sending it to the API to be rejected. @@ -251,54 +238,53 @@ T postToApi(final Class returnType, final String token, final String p { ((Validatable) content).validate(); } + + final HttpRequest request; if (!SonosUtilityHelper.isEmpty(content)) { final String json; - final StringEntity requestContent; try { json = OM.writeValueAsString(content); - requestContent = new StringEntity(json, ContentType.APPLICATION_JSON); } catch (final JsonProcessingException e) { throw new SonosApiClientException("Unable to convert POST request parameters", e); } - request.setEntity(requestContent); + request = builder.setHeader("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) + .build(); + } + else + { + request = builder.POST(HttpRequest.BodyPublishers.noBody()).build(); } return callApi(request, returnType); } /** - * Decorate a request (e.g. {@link HttpGet} or {@link HttpPost}) with the standard URI and Bearer auth - * header used by every Sonos control API call. + * Build up a generic request builder, decorated with the standard URI and Bearer auth header used by + * every Sonos control API call. The caller finishes the builder (e.g. {@code .GET().build()}). * - * @param request the request to decorate - * @param token for the user - * @param path for the API resource - * @param for the requestType - * @return the same request instance, decorated + * @param token for the user + * @param path for the API resource + * @return a decorated, not-yet-finished request builder * @throws SonosApiClientException if an error occurs building the request */ - T getStandardRequest(final T request, final String token, - final String path) throws SonosApiClientException + HttpRequest.Builder getStandardRequest(final String token, final String path) throws SonosApiClientException { final SonosApiConfiguration configuration = apiClient.getConfiguration(); - final URIBuilder uri = new URIBuilder(); - uri.setScheme("https"); - uri.setHost(configuration.getControlBaseUrl()); - uri.setPath(path); - - request.setHeader("Authorization", String.format("Bearer %s", token)); - + final URI uri; try { - request.setURI(uri.build()); - } catch (final URISyntaxException e) + uri = URI.create("https://" + configuration.getControlBaseUrl() + path); + } catch (final IllegalArgumentException e) { throw new SonosApiClientException("Invalid URI built", e); } - return request; + return HttpRequest.newBuilder(uri) + .setHeader("Authorization", String.format("Bearer %s", token)) + .setHeader("User-Agent", apiClient.getUserAgent()); } /** @@ -309,35 +295,22 @@ T getStandardRequest(final T request, final String t * @return a basic GET request * @throws SonosApiClientException if an error occurs building the request */ - HttpGet getGetRequest(final String token, final String path) throws SonosApiClientException + HttpRequest getGetRequest(final String token, final String path) throws SonosApiClientException { - return getStandardRequest(new HttpGet(), token, path); + return getStandardRequest(token, path).GET().build(); } /** - * Helper method to generate a basic GET request + * Helper method to generate a basic DELETE request * * @param token for the user * @param path for the API resource * @return a basic DELETE request * @throws SonosApiClientException if an error occurs building the request */ - HttpDelete getDeleteRequest(final String token, final String path) throws SonosApiClientException - { - return getStandardRequest(new HttpDelete(), token, path); - } - - /** - * Helper method to generate a basic POST request - * - * @param token for the user - * @param path for the API resource - * @return a basic POST request - * @throws SonosApiClientException if an error occurs building the request - */ - HttpPost getPostRequest(final String token, final String path) throws SonosApiClientException + HttpRequest getDeleteRequest(final String token, final String path) throws SonosApiClientException { - return getStandardRequest(new HttpPost(), token, path); + return getStandardRequest(token, path).DELETE().build(); } void validateNotNull(final Object o) throws SonosApiClientException diff --git a/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java b/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java index 6771bc3..9641db2 100644 --- a/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java +++ b/src/main/java/engineer/nightowl/sonos/api/util/SonosCallbackHelper.java @@ -2,17 +2,13 @@ import engineer.nightowl.sonos.api.SonosApiClient; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.Header; -import org.apache.http.HttpMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Base64; import java.util.Map; -import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; @@ -80,25 +76,4 @@ public static Boolean verifySignature(final Map headers, final S { return SonosCallbackHelper.verifySignature(headers, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret()); } - - public static Boolean verifySignature(final HttpMessage message, final SonosApiClient apiClient) throws SonosApiClientException - { - return SonosCallbackHelper.verifySignature(message, apiClient.getConfiguration().getApiKey(), apiClient.getConfiguration().getApiSecret()); - } - - public static Boolean verifySignature(final HttpMessage message, final String apiKey, final String apiSecret) throws SonosApiClientException - { - // Map of headers - Name, Value - Map headers = convertHeadersToMap(message.getAllHeaders()); - return SonosCallbackHelper.verifySignature(headers, apiKey, apiSecret); - } - - public static Map convertHeadersToMap(final Header[] headers) - { - // HTTP permits repeated header names (e.g. behind a proxy/load balancer) - keep the first value - // seen rather than letting Collectors.toMap throw IllegalStateException on a duplicate key. - return Arrays - .stream(headers) - .collect(Collectors.toMap(Header::getName, Header::getValue, (first, second) -> first)); - } } diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/AudioClipResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/AudioClipResourceTest.java index 22d29c7..9a707bb 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/AudioClipResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/AudioClipResourceTest.java @@ -4,7 +4,7 @@ import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.enums.SonosPriority; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,8 +37,8 @@ void testLoadAudioClip() throws Exception final SonosAudioClip result = resource.loadAudioClip("token123", "player1", request); assertEquals("clip1", result.getId()); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/audioClip", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/audioClip", sent.uri().getPath()); } @Test @@ -56,8 +56,8 @@ void testCancelAudioClip() throws Exception final SonosSuccess result = resource.cancelAudioClip("token123", "player1", "clip1"); assertTrue(result.getSuccess()); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/audioClip/clip1", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/audioClip/clip1", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/AuthorizeResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/AuthorizeResourceTest.java index a06e828..a77c94b 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/AuthorizeResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/AuthorizeResourceTest.java @@ -2,16 +2,14 @@ import engineer.nightowl.sonos.api.BaseTestSetup; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import java.net.URI; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.List; class AuthorizeResourceTest extends BaseTestSetup { @@ -30,11 +28,14 @@ public void testGetAuthorizeCodeWithState() throws SonosApiClientException final String state = authorizeResource.generateStateValue(); final URI uri = authorizeResource.getAuthorizeCodeUri("https://localhost", state); assertEquals("https", uri.getScheme()); - uri.getQuery(); - final List params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8); final HashMap map = new HashMap<>(); - params.forEach(p -> map.put(p.getName(), p.getValue())); + for (final String pair : uri.getQuery().split("&")) + { + final String[] kv = pair.split("=", 2); + map.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), + kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : ""); + } assertEquals(state, map.get("state")); assertEquals(configuration.getApiKey(), map.get("client_id")); diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/BaseResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/BaseResourceTest.java index b50b544..74fb578 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/BaseResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/BaseResourceTest.java @@ -8,27 +8,23 @@ import engineer.nightowl.sonos.api.enums.SonosType; import engineer.nightowl.sonos.api.exception.SonosApiClientException; import engineer.nightowl.sonos.api.exception.SonosApiError; -import org.apache.http.HttpEntity; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicStatusLine; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; -import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class BaseResourceTest @@ -36,27 +32,32 @@ public class BaseResourceTest private static BaseResource baseResource; private static SonosApiClient client; private static SonosApiConfiguration configuration; - private static CloseableHttpClient mockedClient; + private static HttpClient mockedClient; @BeforeAll public static void setUp() throws Exception { client = mock(SonosApiClient.class); configuration = mock(SonosApiConfiguration.class); - mockedClient = mock(CloseableHttpClient.class); + mockedClient = mock(HttpClient.class); baseResource = new BaseResource(client); when(client.getConfiguration()).thenReturn(configuration); when(client.getHttpClient()).thenReturn(mockedClient); - when(configuration.getControlBaseUrl()).thenReturn("/control/api"); + when(client.getUserAgent()).thenReturn("sonos-api-java-test"); + when(configuration.getControlBaseUrl()).thenReturn("api.example.com/control/api"); + } + + private static HttpHeaders headersOf(final String name, final String value) + { + return HttpHeaders.of(Map.of(name, List.of(value)), (a, b) -> true); } @Test void getTypeFromHeader() throws SonosApiClientException { - final CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getFirstHeader(BaseResource.SONOS_TYPE_HEADER)) - .thenReturn(new BasicHeader(BaseResource.SONOS_TYPE_HEADER, "homeTheaterOptions")); + final HttpResponse response = mock(HttpResponse.class); + when(response.headers()).thenReturn(headersOf(BaseResource.SONOS_TYPE_HEADER, "homeTheaterOptions")); final SonosType type = baseResource.getTypeFromHeader(response); assertEquals(SonosType.homeTheaterOptions, type); @@ -65,9 +66,8 @@ void getTypeFromHeader() throws SonosApiClientException @Test void getInvalidTypeFromHeader() throws SonosApiClientException { - final CloseableHttpResponse response = mock(CloseableHttpResponse.class); - when(response.getFirstHeader(BaseResource.SONOS_TYPE_HEADER)) - .thenReturn(new BasicHeader(BaseResource.SONOS_TYPE_HEADER, "unexpectedValue")); + final HttpResponse response = mock(HttpResponse.class); + when(response.headers()).thenReturn(headersOf(BaseResource.SONOS_TYPE_HEADER, "unexpectedValue")); try { @@ -81,7 +81,7 @@ void getInvalidTypeFromHeader() throws SonosApiClientException } @Test - void testThatMainApiCallWorks() throws IOException, SonosApiClientException, SonosApiError + void testThatMainApiCallWorks() throws Exception { // Test data final SonosHomeTheaterOptions options = new SonosHomeTheaterOptions(); @@ -90,22 +90,26 @@ void testThatMainApiCallWorks() throws IOException, SonosApiClientException, Son options.setGroupingLatency(50); // Mocks - final HttpEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(options), ContentType.APPLICATION_JSON); + final byte[] bytes = new ObjectMapper().writeValueAsString(options).getBytes(StandardCharsets.UTF_8); - final CloseableHttpResponse mockedResponse = mock(CloseableHttpResponse.class); - when(mockedResponse.getEntity()).thenReturn(entity); - final StatusLine sl = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, null); - when(mockedResponse.getStatusLine()).thenReturn(sl); - when(client.getHttpClient().execute(any())).thenReturn(mockedResponse); + final HttpResponse mockedResponse = mock(HttpResponse.class); + when(mockedResponse.body()).thenReturn(bytes); + when(mockedResponse.statusCode()).thenReturn(200); + when(mockedResponse.headers()).thenReturn(HttpHeaders.of(Map.of(), (a, b) -> true)); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())).thenReturn(mockedResponse); final SonosHomeTheaterOptions responseOptions = baseResource.getFromApi(SonosHomeTheaterOptions.class, "token123", "some/test"); assertEquals(options, responseOptions); - verify(mockedResponse).close(); + // No response.close() verification here: java.net.http.HttpResponse has no close()/Closeable + // contract - with BodyHandlers.ofByteArray() the body is fully buffered before send() returns, so + // there is no connection/stream left open to leak. This test (added in 8a11601, "Verify response + // is closed after successful and 401 calls") has nothing left to assert and is intentionally not + // ported as part of the java.net.http migration. } @Test - void testSonosNotDeclaringTypeStillWorks() throws IOException, SonosApiClientException, SonosApiError + void testSonosNotDeclaringTypeStillWorks() throws Exception { // Test data final SonosHomeTheaterOptions options = new SonosHomeTheaterOptions(); @@ -114,15 +118,13 @@ void testSonosNotDeclaringTypeStillWorks() throws IOException, SonosApiClientExc options.setGroupingLatency(50); // Mocks - final HttpEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(options), ContentType.APPLICATION_JSON); - - final CloseableHttpResponse mockedResponse = mock(CloseableHttpResponse.class); - when(mockedResponse.getEntity()).thenReturn(entity); - final StatusLine sl = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, null); - when(mockedResponse.getStatusLine()).thenReturn(sl); - when(mockedResponse.getFirstHeader(BaseResource.SONOS_TYPE_HEADER)) - .thenReturn(null); - when(client.getHttpClient().execute(any())).thenReturn(mockedResponse); + final byte[] bytes = new ObjectMapper().writeValueAsString(options).getBytes(StandardCharsets.UTF_8); + + final HttpResponse mockedResponse = mock(HttpResponse.class); + when(mockedResponse.body()).thenReturn(bytes); + when(mockedResponse.statusCode()).thenReturn(200); + when(mockedResponse.headers()).thenReturn(HttpHeaders.of(Map.of(), (a, b) -> true)); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())).thenReturn(mockedResponse); final SonosHomeTheaterOptions responseOptions = baseResource.getFromApi(SonosHomeTheaterOptions.class, "token123", "some/test"); @@ -130,12 +132,11 @@ void testSonosNotDeclaringTypeStillWorks() throws IOException, SonosApiClientExc } @Test - void testAuthErrorThrown() throws IOException, SonosApiClientException, SonosApiError + void testAuthErrorThrown() throws Exception { - final CloseableHttpResponse mockedResponse = mock(CloseableHttpResponse.class); - final StatusLine sl = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 401, null); - when(mockedResponse.getStatusLine()).thenReturn(sl); - when(client.getHttpClient().execute(any())).thenReturn(mockedResponse); + final HttpResponse mockedResponse = mock(HttpResponse.class); + when(mockedResponse.statusCode()).thenReturn(401); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())).thenReturn(mockedResponse); try { baseResource.getFromApi(SonosHomeTheaterOptions.class, "token123", "some/test"); @@ -145,27 +146,24 @@ void testAuthErrorThrown() throws IOException, SonosApiClientException, SonosApi { assertEquals("Invalid token", e.getMessage()); } - verify(mockedResponse).close(); + // See testThatMainApiCallWorks() - no response.close() verification needed anymore. } @Test - void testApiErrorHandledCorrectly() throws IOException, SonosApiClientException, SonosApiError + void testApiErrorHandledCorrectly() throws Exception { // Test data final SonosApiError error = new SonosApiError(); error.setErrorCode(SonosErrorCode.ERROR_NOT_CAPABLE); error.setReason("Some test information"); - final HttpEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(error), - ContentType.APPLICATION_JSON); + final byte[] bytes = new ObjectMapper().writeValueAsString(error).getBytes(StandardCharsets.UTF_8); - final CloseableHttpResponse mockedResponse = mock(CloseableHttpResponse.class); - when(mockedResponse.getEntity()).thenReturn(entity); - final StatusLine sl = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, null); - when(mockedResponse.getStatusLine()).thenReturn(sl); - when(mockedResponse.getFirstHeader(BaseResource.SONOS_TYPE_HEADER)) - .thenReturn(new BasicHeader(BaseResource.SONOS_TYPE_HEADER, "globalError")); - when(client.getHttpClient().execute(any())).thenReturn(mockedResponse); + final HttpResponse mockedResponse = mock(HttpResponse.class); + when(mockedResponse.body()).thenReturn(bytes); + when(mockedResponse.statusCode()).thenReturn(500); + when(mockedResponse.headers()).thenReturn(headersOf(BaseResource.SONOS_TYPE_HEADER, "globalError")); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())).thenReturn(mockedResponse); try { baseResource.getFromApi(SonosHomeTheaterOptions.class, "token123", "some/test"); @@ -178,7 +176,7 @@ void testApiErrorHandledCorrectly() throws IOException, SonosApiClientException, } @Test - void testApiMismatchHandledCorrectly() throws IOException, SonosApiClientException, SonosApiError + void testApiMismatchHandledCorrectly() throws Exception { // Test data final SonosHomeTheaterOptions options = new SonosHomeTheaterOptions(); @@ -186,16 +184,13 @@ void testApiMismatchHandledCorrectly() throws IOException, SonosApiClientExcepti options.setEnhanceDialog(true); options.setGroupingLatency(50); - final HttpEntity entity = new StringEntity(new ObjectMapper().writeValueAsString(options), - ContentType.APPLICATION_JSON); + final byte[] bytes = new ObjectMapper().writeValueAsString(options).getBytes(StandardCharsets.UTF_8); - final CloseableHttpResponse mockedResponse = mock(CloseableHttpResponse.class); - when(mockedResponse.getEntity()).thenReturn(entity); - final StatusLine sl = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, null); - when(mockedResponse.getStatusLine()).thenReturn(sl); - when(mockedResponse.getFirstHeader(BaseResource.SONOS_TYPE_HEADER)) - .thenReturn(new BasicHeader(BaseResource.SONOS_TYPE_HEADER, "audioClip")); - when(client.getHttpClient().execute(any())).thenReturn(mockedResponse); + final HttpResponse mockedResponse = mock(HttpResponse.class); + when(mockedResponse.body()).thenReturn(bytes); + when(mockedResponse.statusCode()).thenReturn(200); + when(mockedResponse.headers()).thenReturn(headersOf(BaseResource.SONOS_TYPE_HEADER, "audioClip")); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())).thenReturn(mockedResponse); try { baseResource.getFromApi(SonosHomeTheaterOptions.class, "token123", "some/test"); @@ -210,16 +205,12 @@ void testApiMismatchHandledCorrectly() throws IOException, SonosApiClientExcepti @Test void getStandardRequest() throws SonosApiClientException { - final HttpGet req = baseResource.getStandardRequest(new HttpGet(), "token123", "/some/path"); - assertEquals(HttpGet.METHOD_NAME, - req.getMethod()); + final HttpRequest req = baseResource.getStandardRequest("token123", "/some/path").GET().build(); + assertEquals("GET", req.method()); - assertEquals("Bearer token123", - req.getFirstHeader("Authorization").getValue()); + assertEquals("Bearer token123", req.headers().firstValue("Authorization").orElse(null)); - assertEquals( - "/control/api/some/path", - req.getURI().getPath()); + assertEquals("/control/api/some/path", req.uri().getPath()); } @Test @@ -251,4 +242,4 @@ void testValidateNotNullNonEmptyObject() throws SonosApiClientException { baseResource.validateNotNull("nonEmptyString", "sonos-api-java"); } -} \ No newline at end of file +} diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/FavoriteResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/FavoriteResourceTest.java index 5325dee..47925f9 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/FavoriteResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/FavoriteResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosFavoriteList; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,8 +28,8 @@ void testGetFavorites() throws Exception resource.getFavorites("token123", "household1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/households/household1/favorites", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/households/household1/favorites", sent.uri().getPath()); } @Test @@ -46,8 +46,8 @@ void testLoadFavorite() throws Exception final SonosSuccess result = resource.loadFavorite("token123", "group1", "favorite1", true, null); assertTrue(result.getSuccess()); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/favorites", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/favorites", sent.uri().getPath()); assertTrue(readRequestBody(sent).contains("favorite1")); } diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/GroupResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/GroupResourceTest.java index 2f117f9..2403f56 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/GroupResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/GroupResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosGroupInfo; import engineer.nightowl.sonos.api.domain.SonosGroups; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,8 +30,8 @@ void testGetGroups() throws Exception resource.getGroups("token123", "household1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/households/household1/groups", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/households/household1/groups", sent.uri().getPath()); } @Test @@ -48,8 +48,8 @@ void testCreateGroup() throws Exception resource.createGroup("token123", "household1", playerIds, null); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/households/household1/groups/createGroup", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/households/household1/groups/createGroup", sent.uri().getPath()); } @Test @@ -73,8 +73,8 @@ void testModifyGroupMembers() throws Exception resource.modifyGroupMembers("token123", "group1", Collections.singletonList("player1"), null); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groups/modifyGroupMembers", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groups/modifyGroupMembers", sent.uri().getPath()); } @Test @@ -98,8 +98,8 @@ void testSetGroupMembers() throws Exception resource.setGroupMembers("token123", "group1", Collections.singletonList("player1")); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groups/setGroupMembers", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groups/setGroupMembers", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/GroupVolumeResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/GroupVolumeResourceTest.java index 0537307..af744f0 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/GroupVolumeResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/GroupVolumeResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosGroupVolume; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,8 +27,8 @@ void testGetVolume() throws Exception resource.getVolume("token123", "group1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groupVolume", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groupVolume", sent.uri().getPath()); } @Test @@ -44,8 +44,8 @@ void testSetVolume() throws Exception resource.setVolume("token123", "group1", 50); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groupVolume", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groupVolume", sent.uri().getPath()); } @Test @@ -61,8 +61,8 @@ void testSetRelativeVolume() throws Exception resource.setRelativeVolume("token123", "group1", 10); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groupVolume/relative", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groupVolume/relative", sent.uri().getPath()); } @Test @@ -78,8 +78,8 @@ void testSetMute() throws Exception resource.setMute("token123", "group1", true); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/groupVolume/mute", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/groupVolume/mute", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/HomeTheaterResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/HomeTheaterResourceTest.java index 57c2b86..6de991a 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/HomeTheaterResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/HomeTheaterResourceTest.java @@ -4,7 +4,7 @@ import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.enums.SonosTvPowerState; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,8 +28,8 @@ void testLoadHomeTheaterPlayback() throws Exception resource.loadHomeTheaterPlayback("token123", "player1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/homeTheater", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/homeTheater", sent.uri().getPath()); } @Test @@ -45,8 +45,8 @@ void testSetTvPowerState() throws Exception resource.setTvPowerState("token123", "player1", SonosTvPowerState.ON); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/homeTheater/tvPowerState", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/homeTheater/tvPowerState", sent.uri().getPath()); } @Test @@ -63,8 +63,8 @@ void testGetOptions() throws Exception resource.getOptions("token123", "player1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/homeTheater/options", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/homeTheater/options", sent.uri().getPath()); } @Test @@ -80,8 +80,8 @@ void testSetOptions() throws Exception resource.setOptions("token123", "player1", new SonosHomeTheaterOptions()); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/players/player1/homeTheater/options", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/players/player1/homeTheater/options", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/HouseholdResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/HouseholdResourceTest.java index 8ade67c..d21d720 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/HouseholdResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/HouseholdResourceTest.java @@ -1,7 +1,7 @@ package engineer.nightowl.sonos.api.resource; import engineer.nightowl.sonos.api.domain.SonosHouseholdList; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,7 +24,7 @@ void testGetHouseholds() throws Exception resource.getHouseholds("token123"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/households", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/households", sent.uri().getPath()); } } diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/MockedApiTestSetup.java b/src/test/java/engineer/nightowl/sonos/api/resource/MockedApiTestSetup.java index f0fa825..0c121db 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/MockedApiTestSetup.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/MockedApiTestSetup.java @@ -3,19 +3,22 @@ import com.fasterxml.jackson.databind.ObjectMapper; import engineer.nightowl.sonos.api.SonosApiClient; import engineer.nightowl.sonos.api.SonosApiConfiguration; -import org.apache.http.HttpEntity; -import org.apache.http.ProtocolVersion; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicStatusLine; import org.junit.jupiter.api.BeforeEach; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -23,7 +26,7 @@ import static org.mockito.Mockito.when; /** - * Shared Mockito-based harness for resource tests - mocks the {@link CloseableHttpClient} at the seam + * Shared Mockito-based harness for resource tests - mocks the {@link HttpClient} at the seam * {@link BaseResource} uses, so no real network calls are made. Subclasses get a configured * {@link SonosApiClient} plus helpers to stub a JSON response and capture the request that was sent. */ @@ -34,17 +37,18 @@ abstract class MockedApiTestSetup SonosApiClient client; SonosApiConfiguration configuration; - CloseableHttpClient mockedClient; + HttpClient mockedClient; @BeforeEach void setUpMockedApiClient() { client = mock(SonosApiClient.class); configuration = mock(SonosApiConfiguration.class); - mockedClient = mock(CloseableHttpClient.class); + mockedClient = mock(HttpClient.class); when(client.getConfiguration()).thenReturn(configuration); when(client.getHttpClient()).thenReturn(mockedClient); + when(client.getUserAgent()).thenReturn("sonos-api-java-test"); when(configuration.getControlBaseUrl()).thenReturn(CONTROL_BASE_URL); } @@ -54,13 +58,18 @@ void setUpMockedApiClient() * @param body the object to serialize as the response body * @return the mocked response, in case further stubbing (e.g. a type header) is needed */ - CloseableHttpResponse stubJsonResponse(final Object body) throws IOException + @SuppressWarnings("unchecked") + HttpResponse stubJsonResponse(final Object body) throws IOException, InterruptedException { - final CloseableHttpResponse response = mock(CloseableHttpResponse.class); - final HttpEntity entity = new StringEntity(OBJECT_MAPPER.writeValueAsString(body), ContentType.APPLICATION_JSON); - when(response.getEntity()).thenReturn(entity); - when(response.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, null)); - when(mockedClient.execute(any())).thenReturn(response); + final HttpResponse response = mock(HttpResponse.class); + final byte[] bytes = OBJECT_MAPPER.writeValueAsString(body).getBytes(StandardCharsets.UTF_8); + when(response.statusCode()).thenReturn(200); + when(response.body()).thenReturn(bytes); + // Unlike Apache's getFirstHeader() (tolerant of an unstubbed mock returning null), an unstubbed + // headers() call here returns null by default, which would NPE inside BaseResource.getTypeFromHeader. + when(response.headers()).thenReturn(HttpHeaders.of(Map.of(), (a, b) -> true)); + when(mockedClient.send(any(HttpRequest.class), ArgumentMatchers.>any())) + .thenReturn(response); return response; } @@ -70,31 +79,62 @@ CloseableHttpResponse stubJsonResponse(final Object body) throws IOException * * @return the captured request */ - HttpUriRequest captureRequest() throws IOException + HttpRequest captureRequest() throws IOException, InterruptedException { - final ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequest.class); - verify(mockedClient).execute(captor.capture()); + final ArgumentCaptor captor = ArgumentCaptor.forClass(HttpRequest.class); + verify(mockedClient).send(captor.capture(), any()); return captor.getValue(); } /** - * Read the entity content of a request (e.g. an {@link org.apache.http.client.methods.HttpPost}) as a - * UTF-8 string. + * Read the body of a request (e.g. a POST built with {@code BodyPublishers.ofString(...)}) as a UTF-8 + * string. {@link HttpRequest} has no direct string/byte accessor for its body - only a reactive + * {@link Flow.Publisher}<{@link ByteBuffer}> - so this drains it via a small ad-hoc subscriber. * * @param request the request to read the body of - * @return the body content, or null if the request has no entity + * @return the body content, or null if the request has no body */ - static String readRequestBody(final HttpUriRequest request) throws IOException + static String readRequestBody(final HttpRequest request) { - if (!(request instanceof HttpEntityEnclosingRequestBase)) - { - return null; - } - final HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity(); - if (entity == null) + return request.bodyPublisher() + .map(MockedApiTestSetup::drain) + .orElse(null); + } + + private static String drain(final HttpRequest.BodyPublisher publisher) + { + final CompletableFuture result = new CompletableFuture<>(); + final List buffers = new ArrayList<>(); + publisher.subscribe(new Flow.Subscriber<>() { - return null; - } - return new String(entity.getContent().readAllBytes()); + @Override + public void onSubscribe(final Flow.Subscription subscription) + { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(final ByteBuffer item) + { + buffers.add(item); + } + + @Override + public void onError(final Throwable throwable) + { + result.completeExceptionally(throwable); + } + + @Override + public void onComplete() + { + final int totalBytes = buffers.stream().mapToInt(ByteBuffer::remaining).sum(); + final ByteBuffer combined = ByteBuffer.allocate(totalBytes); + buffers.forEach(combined::put); + combined.flip(); + result.complete(StandardCharsets.UTF_8.decode(combined).toString()); + } + }); + return result.join(); } } diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/MusicServiceAccountsResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/MusicServiceAccountsResourceTest.java index 4779e5c..fdc8375 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/MusicServiceAccountsResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/MusicServiceAccountsResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosMusicServiceAccount; import engineer.nightowl.sonos.api.domain.SonosMusicServiceAccountMatchRequest; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,8 +29,8 @@ void testMatch() throws Exception resource.match("token123", "household1", request); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/households/household1/musicServiceAccounts/match", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/households/household1/musicServiceAccounts/match", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackMetadataResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackMetadataResourceTest.java index 070dacc..0a74559 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackMetadataResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackMetadataResourceTest.java @@ -2,7 +2,7 @@ import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,8 +26,8 @@ void testSubscribe() throws Exception resource.subscribe("token123", "group1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/playbackMetadata/subscription", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/playbackMetadata/subscription", sent.uri().getPath()); } @Test @@ -43,8 +43,8 @@ void testUnsubscribe() throws Exception resource.unsubscribe("token123", "group1"); - final HttpUriRequest sent = captureRequest(); - assertEquals("/control/api/v1/groups/group1/playbackMetadata/subscription", sent.getURI().getPath()); + final HttpRequest sent = captureRequest(); + assertEquals("/control/api/v1/groups/group1/playbackMetadata/subscription", sent.uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackResourceTest.java index 61892e8..593f895 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackResourceTest.java @@ -4,7 +4,7 @@ import engineer.nightowl.sonos.api.domain.SonosPlaybackStatus; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +28,7 @@ void testGetPlaybackStatus() throws Exception resource.getPlaybackStatus("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback", captureRequest().uri().getPath()); } @Test @@ -44,7 +44,7 @@ void testLoadLineIn() throws Exception resource.loadLineIn("token123", "group1", "device1", true); - assertEquals("/control/api/v1/groups/group1/playback/lineIn", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/lineIn", captureRequest().uri().getPath()); } @Test @@ -60,7 +60,7 @@ void testPlay() throws Exception resource.play("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback/play", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/play", captureRequest().uri().getPath()); } @Test @@ -76,7 +76,7 @@ void testPause() throws Exception resource.pause("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback/pause", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/pause", captureRequest().uri().getPath()); } @Test @@ -92,7 +92,7 @@ void testSeek() throws Exception resource.seek("token123", "group1", "item1", 1000); - assertEquals("/control/api/v1/groups/group1/playback/seek", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/seek", captureRequest().uri().getPath()); } @Test @@ -114,7 +114,7 @@ void testSeekRelative() throws Exception resource.seekRelative("token123", "group1", "item1", 1000); - assertEquals("/control/api/v1/groups/group1/playback/seekRelative", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/seekRelative", captureRequest().uri().getPath()); } @Test @@ -130,7 +130,7 @@ void testSetPlayModes() throws Exception resource.setPlayModes("token123", "group1", new SonosPlayMode()); - assertEquals("/control/api/v1/groups/group1/playback/playMode", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/playMode", captureRequest().uri().getPath()); } @Test @@ -146,7 +146,7 @@ void testSkipToNextTrack() throws Exception resource.skipToNextTrack("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback/skipToNextTrack", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/skipToNextTrack", captureRequest().uri().getPath()); } @Test @@ -162,7 +162,7 @@ void testSkipToPreviousTrack() throws Exception resource.skipToPreviousTrack("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback/skipToPreviousTrack", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/skipToPreviousTrack", captureRequest().uri().getPath()); } @Test @@ -178,7 +178,7 @@ void testTogglePlayPause() throws Exception resource.togglePlayPause("token123", "group1"); - assertEquals("/control/api/v1/groups/group1/playback/togglePlayPause", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playback/togglePlayPause", captureRequest().uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackSessionResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackSessionResourceTest.java index fdccb7c..57769c5 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackSessionResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/PlaybackSessionResourceTest.java @@ -6,7 +6,7 @@ import engineer.nightowl.sonos.api.domain.SonosStreamUrlRequest; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,7 +34,7 @@ void testCreateSession() throws Exception resource.createSession("token123", "group1", sessionRequest); - assertEquals("/control/api/v1/groups/group1/playbackSession", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playbackSession", captureRequest().uri().getPath()); } @Test @@ -50,7 +50,7 @@ void testJoinSession() throws Exception resource.joinSession("token123", "group1", sessionRequest); - assertEquals("/control/api/v1/groups/group1/playbackSession/join", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playbackSession/join", captureRequest().uri().getPath()); } @Test @@ -66,7 +66,7 @@ void testJoinOrCreateSession() throws Exception resource.joinOrCreateSession("token123", "group1", sessionRequest); - assertEquals("/control/api/v1/groups/group1/playbackSession/joinOrCreate", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playbackSession/joinOrCreate", captureRequest().uri().getPath()); } @Test @@ -86,7 +86,7 @@ void testLoadCloudQueue() throws Exception resource.loadCloudQueue("token123", "session1", request); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/loadCloudQueue", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -108,7 +108,7 @@ void testLoadStreamUrl() throws Exception resource.loadStreamUrl("token123", "session1", request); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/loadStreamUrl", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -128,7 +128,7 @@ void testRefreshCloudQueue() throws Exception resource.refreshCloudQueue("token123", "session1"); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/refreshCloudQueue", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -145,7 +145,7 @@ void testSkipToItem() throws Exception resource.skipToItem("token123", "session1", sessionRequest); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/skipToItem", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -162,7 +162,7 @@ void testSeek() throws Exception resource.seek("token123", "session1", "item1", 1000); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/seek", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -185,7 +185,7 @@ void testSeekRelative() throws Exception resource.seekRelative("token123", "session1", "item1", 1000); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/seekRelative", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test @@ -202,7 +202,7 @@ void testSuspend() throws Exception resource.suspend("token123", "session1", "v1"); assertEquals("/control/api/v1/playbackSessions/session1/playbackSession/suspend", - captureRequest().getURI().getPath()); + captureRequest().uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/PlayerVolumeResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/PlayerVolumeResourceTest.java index c83123a..4b38ed6 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/PlayerVolumeResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/PlayerVolumeResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosPlayerVolume; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,7 +27,7 @@ void testGetVolume() throws Exception resource.getVolume("token123", "player1"); - assertEquals("/control/api/v1/players/player1/playerVolume", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/playerVolume", captureRequest().uri().getPath()); } @Test @@ -43,7 +43,7 @@ void testSetMute() throws Exception resource.setMute("token123", "player1", true); - assertEquals("/control/api/v1/players/player1/playerVolume/mute", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/playerVolume/mute", captureRequest().uri().getPath()); } @Test @@ -59,7 +59,7 @@ void testSetRelativeVolume() throws Exception resource.setRelativeVolume("token123", "player1", 10, false); - assertEquals("/control/api/v1/players/player1/playerVolume/relative", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/playerVolume/relative", captureRequest().uri().getPath()); } @Test @@ -75,7 +75,7 @@ void testSetVolume() throws Exception resource.setVolume("token123", "player1", 50, false); - assertEquals("/control/api/v1/players/player1/playerVolume", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/playerVolume", captureRequest().uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/PlaylistResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/PlaylistResourceTest.java index 07a578a..56b731e 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/PlaylistResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/PlaylistResourceTest.java @@ -4,7 +4,7 @@ import engineer.nightowl.sonos.api.domain.SonosPlaylistList; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +28,7 @@ void testGetPlaylists() throws Exception resource.getPlaylists("token123", "household1"); - assertEquals("/control/api/v1/households/household1/playlists", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/households/household1/playlists", captureRequest().uri().getPath()); } @Test @@ -44,7 +44,7 @@ void testGetPlaylist() throws Exception resource.getPlaylist("token123", "household1", "playlist1"); - assertEquals("/control/api/v1/households/household1/playlists/playlist1", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/households/household1/playlists/playlist1", captureRequest().uri().getPath()); } @Test @@ -66,7 +66,7 @@ void testLoadPlaylist() throws Exception resource.loadPlaylist("token123", "group1", "playlist1", true, null); - assertEquals("/control/api/v1/groups/group1/playlists", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/groups/group1/playlists", captureRequest().uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/resource/SettingsResourceTest.java b/src/test/java/engineer/nightowl/sonos/api/resource/SettingsResourceTest.java index 5f56a17..da16623 100644 --- a/src/test/java/engineer/nightowl/sonos/api/resource/SettingsResourceTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/resource/SettingsResourceTest.java @@ -3,7 +3,7 @@ import engineer.nightowl.sonos.api.domain.SonosPlayerSettings; import engineer.nightowl.sonos.api.domain.SonosSuccess; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.client.methods.HttpUriRequest; +import java.net.http.HttpRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,7 +27,7 @@ void testGetSettings() throws Exception resource.getSettings("token123", "player1"); - assertEquals("/control/api/v1/players/player1/settings/player", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/settings/player", captureRequest().uri().getPath()); } @Test @@ -43,7 +43,7 @@ void testSetSettings() throws Exception resource.setSettings("token123", "player1", new SonosPlayerSettings()); - assertEquals("/control/api/v1/players/player1/settings/player", captureRequest().getURI().getPath()); + assertEquals("/control/api/v1/players/player1/settings/player", captureRequest().uri().getPath()); } @Test diff --git a/src/test/java/engineer/nightowl/sonos/api/util/SonosCallbackHelperTest.java b/src/test/java/engineer/nightowl/sonos/api/util/SonosCallbackHelperTest.java index f1491fd..b0e9ea2 100644 --- a/src/test/java/engineer/nightowl/sonos/api/util/SonosCallbackHelperTest.java +++ b/src/test/java/engineer/nightowl/sonos/api/util/SonosCallbackHelperTest.java @@ -1,12 +1,8 @@ package engineer.nightowl.sonos.api.util; import engineer.nightowl.sonos.api.exception.SonosApiClientException; -import org.apache.http.Header; -import org.apache.http.entity.ContentType; -import org.apache.http.message.BasicHeader; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,7 +25,7 @@ private Map getHeaders() headers.put("X-Sonos-Target-Type", "target-type"); headers.put("X-Sonos-Target-Value", "target-value"); headers.put("Unrelated-Header", "sonos123"); - headers.put("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); + headers.put("Content-Type", "application/json"); return headers; } @@ -70,17 +66,4 @@ void testVerifySignatureWithMissingHeaderThrowsClientException() () -> SonosCallbackHelper.verifySignature(headers, apiKey, apiSecret)); assertTrue(exception.getMessage().contains("X-Sonos-Type")); } - - @Test - void testConvertHeadersToMapKeepsFirstValueOnDuplicateHeaderName() - { - final Header[] headers = new Header[] { - new BasicHeader("X-Sonos-Type", "first"), - new BasicHeader("X-Sonos-Type", "second") - }; - - final Map result = SonosCallbackHelper.convertHeadersToMap(headers); - - assertEquals("first", result.get("X-Sonos-Type")); - } } \ No newline at end of file