package com.soundcloud.api; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.soundcloud.api.fakehttp.FakeHttpLayer; import com.soundcloud.api.fakehttp.RequestMatcher; import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.AuthenticationHandler; import org.apache.http.client.HttpClient; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.RedirectHandler; import org.apache.http.client.RequestDirector; import org.apache.http.client.UserTokenHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpProcessor; import org.apache.http.protocol.HttpRequestExecutor; import org.junit.Before; import org.junit.Test; import java.io.File; import java.io.IOException; import java.net.URI; public class ApiWrapperTest { private ApiWrapper api; private final static String TEST_CLIENT_ID = "testClientId"; private final static String TEST_CLIENT_SECRET = "testClientSecret"; final FakeHttpLayer layer = new FakeHttpLayer(); @Before public void setup() { api = new ApiWrapper(TEST_CLIENT_ID, TEST_CLIENT_SECRET, URI.create("redirect://me"), null) { private static final long serialVersionUID = 12345; // silence warnings @Override protected RequestDirector getRequestDirector(HttpRequestExecutor requestExec, ClientConnectionManager conman, ConnectionReuseStrategy reustrat, ConnectionKeepAliveStrategy kastrat, HttpRoutePlanner rouplan, HttpProcessor httpProcessor, HttpRequestRetryHandler retryHandler, RedirectHandler redirectHandler, AuthenticationHandler targetAuthHandler, AuthenticationHandler proxyAuthHandler, UserTokenHandler stateHandler, HttpParams params) { return new RequestDirector() { @Override public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) throws HttpException, IOException { return layer.emulateRequest(target, request, context, this); } }; } }; layer.clearHttpResponseRules(); } @Test(expected = IllegalArgumentException.class) public void loginShouldThrowIllegalArgumentException() throws Exception { api.login(null, null); } @Test public void clientCredentialsShouldDefaultToSignupScope() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": \"signup\",\n" + " \"refresh_token\": \"04u7h-r3fr35h-70k3n\"\n" + "}"); Token t = api.clientCredentials(); assertThat(t.access, equalTo("04u7h-4cc355-70k3n")); assertThat(t.refresh, equalTo("04u7h-r3fr35h-70k3n")); assertThat(t.scope, equalTo("signup")); assertTrue(t.signupScoped()); assertNotNull(t.getExpiresIn()); } @Test(expected = CloudAPI.InvalidTokenException.class) public void clientCredentialsShouldThrowIfScopeCanNotBeObtained() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": \"loser\",\n" + " \"refresh_token\": \"04u7h-r3fr35h-70k3n\"\n" + "}"); api.clientCredentials("unlimitedammo"); } @Test public void shouldGetTokensWhenLoggingIn() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": \"*\",\n" + " \"refresh_token\": \"04u7h-r3fr35h-70k3n\"\n" + "}"); Token t = api.login("foo", "bar"); assertThat(t.access, equalTo("04u7h-4cc355-70k3n")); assertThat(t.refresh, equalTo("04u7h-r3fr35h-70k3n")); assertThat(t.scope, equalTo("*")); assertNotNull(t.getExpiresIn()); } @Test public void shouldGetTokensWhenLoggingInWithNonExpiringScope() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"scope\": \"* non-expiring\"\n" + "}"); Token t = api.login("foo", "bar", Token.SCOPE_NON_EXPIRING); assertThat(t.access, equalTo("04u7h-4cc355-70k3n")); assertThat(t.refresh, is(nullValue())); assertThat(t.getExpiresIn(), is(nullValue())); assertThat(t.scoped(Token.SCOPE_NON_EXPIRING), is(true)); assertThat(t.scoped(Token.SCOPE_DEFAULT), is(true)); } @Test public void shouldGetTokensWhenLoggingInViaAuthorizationCode() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": \"*\",\n" + " \"refresh_token\": \"04u7h-r3fr35h-70k3n\"\n" + "}"); Token t = api.authorizationCode("code"); assertThat(t.access, equalTo("04u7h-4cc355-70k3n")); assertThat(t.refresh, equalTo("04u7h-r3fr35h-70k3n")); assertThat(t.scope, equalTo("*")); assertNotNull(t.getExpiresIn()); } @Test public void shouldGetTokensWhenLoggingInViaAuthorizationCodeAndScope() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"04u7h-4cc355-70k3n\",\n" + " \"scope\": \"* non-expiring\"\n" + "}"); Token t = api.authorizationCode("code", Token.SCOPE_NON_EXPIRING); assertThat(t.access, equalTo("04u7h-4cc355-70k3n")); assertThat(t.refresh, is(nullValue())); assertThat(t.scoped(Token.SCOPE_DEFAULT), is(true)); assertThat(t.scoped(Token.SCOPE_NON_EXPIRING), is(true)); assertNull(t.getExpiresIn()); } @Test(expected = CloudAPI.InvalidTokenException.class) public void shouldThrowInvalidTokenExceptionWhenLoginFailed() throws Exception { layer.addPendingHttpResponse(401, "{\n" + " \"error\": \"Error!\"\n" + "}"); api.login("foo", "bar"); } @Test(expected = CloudAPI.ApiResponseException.class) public void shouldThrowApiResponseExceptionWhenInvalidJSONReturned() throws Exception { layer.addPendingHttpResponse(200, "I'm invalid JSON!"); api.login("foo", "bar"); } @Test public void shouldContainInvalidJSONInExceptionMessage() throws Exception { layer.addPendingHttpResponse(200, "I'm invalid JSON!"); try { api.login("foo", "bar"); fail("expected IOException"); } catch (CloudAPI.ApiResponseException e) { assertThat(e.getMessage(), containsString("I'm invalid JSON!")); } } @Test public void shouldRefreshToken() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"fr3sh\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": null,\n" + " \"refresh_token\": \"refresh\"\n" + "}"); api.setToken(new Token("access", "refresh")); assertThat(api .refreshToken() .access, equalTo("fr3sh")); } @Test(expected = IllegalStateException.class) public void shouldThrowIllegalStateExceptionWhenNoRefreshToken() throws Exception { api.refreshToken(); } @Test public void shouldResolveUris() throws Exception { HttpResponse r = mock(HttpResponse.class); StatusLine line = mock(StatusLine.class); when(line.getStatusCode()).thenReturn(302); when(r.getStatusLine()).thenReturn(line); Header location = mock(Header.class); when(location.getValue()).thenReturn("http://api.soundcloud.com/users/1000"); when(r.getFirstHeader(anyString())).thenReturn(location); layer.addHttpResponseRule(new RequestMatcher() { @Override public boolean matches(HttpRequest request) { return true; } }, r); assertThat(api.resolve("http://soundcloud.com/crazybob"), is(1000L)); } @Test(expected = CloudAPI.ResolverException.class) public void resolveShouldReturnNegativeOneWhenInvalid() throws Exception { layer.addPendingHttpResponse(404, "Not found"); api.resolve("http://soundcloud.com/nonexisto"); } @Test public void shouldGetContent() throws Exception { layer.addHttpResponseRule("/some/resource?a=1&client_id=" + TEST_CLIENT_ID, "response"); assertThat(Http.getString(api.get(Request.to("/some/resource").with("a", "1"))), equalTo("response")); } @Test public void shouldPostContent() throws Exception { HttpResponse resp = mock(HttpResponse.class); layer.addHttpResponseRule("POST", "/foo/something", resp); assertThat(api.post(Request.to("/foo/something").with("a", 1)), equalTo(resp)); } @Test public void shouldPutContent() throws Exception { HttpResponse resp = mock(HttpResponse.class); layer.addHttpResponseRule("PUT", "/foo/something", resp); assertThat(api.put(Request.to("/foo/something").with("a", 1)), equalTo(resp)); } @Test public void shouldDeleteContent() throws Exception { HttpResponse resp = mock(HttpResponse.class); layer.addHttpResponseRule("DELETE", "/foo/something?client_id=" + TEST_CLIENT_ID, resp); assertThat(api.delete(new Request("/foo/something")), equalTo(resp)); } @Test public void testGetOAuthHeader() throws Exception { Header h = ApiWrapper.createOAuthHeader(new Token("foo", "refresh")); assertThat(h.getName(), equalTo("Authorization")); assertThat(h.getValue(), equalTo("OAuth foo")); } @Test public void testGetOAuthHeaderNullToken() throws Exception { Header h = ApiWrapper.createOAuthHeader(null); assertThat(h.getName(), equalTo("Authorization")); assertThat(h.getValue(), equalTo("OAuth invalidated")); } @Test public void shouldGenerateUrlWithoutParameters() throws Exception { assertThat( api.getURI(new Request("/my-resource"), true, true).toString(), equalTo("https://api.soundcloud.com/my-resource") ); } @Test public void shouldGenerateUrlWithoutSSL() throws Exception { assertThat( api.getURI(new Request("/my-resource"), true, false).toString(), equalTo("http://api.soundcloud.com/my-resource") ); } @Test public void shouldGenerateUrlWithParameters() throws Exception { assertThat( api.getURI(Request.to("/my-resource").with("foo", "bar"), true, true).toString(), equalTo("https://api.soundcloud.com/my-resource?foo=bar") ); } @Test public void shouldGenerateUrlForWebHost() throws Exception { assertThat( api.getURI(Request.to("/my-resource"), false, true).toString(), equalTo("https://soundcloud.com/my-resource") ); } @Test public void shouldGenerateURIForLoginAuthCode() throws Exception { assertThat( api.authorizationCodeUrl().toString(), equalTo("https://soundcloud.com/connect"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code") ); } @Test public void shouldGenerateURIForLoginAuthCodeWithDifferentEndPoint() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT).toString(), equalTo("https://soundcloud.com/connect/via/facebook"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code") ); } @Test public void shouldIncludeScopeInAuthorizationUrl() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.FACEBOOK_CONNECT, Token.SCOPE_NON_EXPIRING).toString(), equalTo("https://soundcloud.com/connect/via/facebook"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=non-expiring") ); } @Test public void shouldIncludeDisplayInAuthorizationUrl() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_NON_EXPIRING, CloudAPI.POPUP).toString(), equalTo("https://soundcloud.com/connect"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=non-expiring"+ "&display=popup") ); } @Test public void shouldIncludeStateInAuthorizationUrl() throws Exception { assertThat( api.authorizationCodeUrl(Endpoints.CONNECT, Token.SCOPE_DEFAULT, CloudAPI.POPUP, "stateValue").toString(), equalTo("https://soundcloud.com/connect"+ "?redirect_uri=redirect%3A%2F%2Fme&client_id=" + TEST_CLIENT_ID + "&response_type=code&scope=*"+ "&display=popup&state=stateValue") ); } @Test public void shouldCallTokenStateListenerWhenTokenIsInvalidated() throws Exception { CloudAPI.TokenListener listener = mock(CloudAPI.TokenListener.class); api.setTokenListener(listener); final Token old = api.getToken(); api.invalidateToken(); verify(listener).onTokenInvalid(old); } @Test public void invalidateTokenShouldTryToGetAlternativeToken() throws Exception { CloudAPI.TokenListener listener = mock(CloudAPI.TokenListener.class); final Token cachedToken = new Token("new", "fresh"); api.setTokenListener(listener); when(listener.onTokenInvalid(api.getToken())).thenReturn(cachedToken); assertThat(api.invalidateToken(), equalTo(cachedToken)); } @Test public void invalidateTokenShouldReturnNullIfNoListenerAvailable() throws Exception { assertThat(api.invalidateToken(), is(nullValue())); } @Test public void shouldCallTokenStateListenerWhenTokenIsRefreshed() throws Exception { layer.addPendingHttpResponse(200, "{\n" + " \"access_token\": \"fr3sh\",\n" + " \"expires_in\": 3600,\n" + " \"scope\": null,\n" + " \"refresh_token\": \"refresh\"\n" + "}"); CloudAPI.TokenListener listener = mock(CloudAPI.TokenListener.class); api.setToken(new Token("access", "refresh")); api.setTokenListener(listener); api.refreshToken(); verify(listener).onTokenRefreshed(api.getToken()); } @Test public void shouldSerializeAndDeserializeWrapper() throws Exception { ApiWrapper wrapper = new ApiWrapper("client", "secret", null, new Token("1", "2")); File ser = File.createTempFile("serialized_wrapper", "ser"); wrapper.toFile(ser); ApiWrapper other = ApiWrapper.fromFile(ser); assertThat(wrapper.getToken(), equalTo(other.getToken())); assertThat(wrapper.env, equalTo(other.env)); // make sure we can still use listeners after deserializing CloudAPI.TokenListener listener = mock(CloudAPI.TokenListener.class); other.setTokenListener(listener); final Token old = other.getToken(); other.invalidateToken(); verify(listener).onTokenInvalid(old); } @Test public void testAddScope() throws Exception { assertThat(ApiWrapper.addScope(new Request(), new String[] { "foo", "bar"}).getParams().get("scope"), equalTo("foo bar")); assertFalse(ApiWrapper.addScope(new Request(), new String[] {}).getParams().containsKey("scope")); assertFalse(ApiWrapper.addScope(new Request(), null).getParams().containsKey("scope")); } @Test public void shouldSetProxy() throws Exception { assertFalse(api.isProxySet()); URI proxy = URI.create("https://foo.com"); assertEquals(proxy.getPort(), -1); api.setProxy(proxy); assertTrue(api.isProxySet()); assertEquals("https://foo.com:443", api.getProxy().toString()); api.setProxy(URI.create("https://foo.com:12345")); assertEquals(URI.create("https://foo.com:12345"), api.getProxy()); } @Test @SuppressWarnings("serial") public void shouldHandleBrokenHttpClientNPE() throws Exception { final HttpClient client = mock(HttpClient.class); ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; } }; when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new NullPointerException()); try { broken.execute(new HttpGet("/foo")); fail("expected BrokenHttpClientException"); } catch (ApiWrapper.BrokenHttpClientException expected) { // make sure client retried request verify(client, times(2)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } } @Test @SuppressWarnings("serial") public void shouldHandleBrokenHttpClientIAE() throws Exception { final HttpClient client = mock(HttpClient.class); ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; } }; when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new IllegalArgumentException()); try { broken.execute(new HttpGet("/foo")); fail("expected BrokenHttpClientException"); } catch (ApiWrapper.BrokenHttpClientException expected) { verify(client, times(1)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } } @SuppressWarnings("serial") @Test public void shouldSafeExecute() throws Exception { final HttpClient client = mock(HttpClient.class); ApiWrapper broken = new ApiWrapper("invalid", "invalid", URI.create("redirect://me"), null) { @Override public HttpClient getHttpClient() { return client; } }; when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new IllegalArgumentException()); try { broken.safeExecute(null, new HttpGet("/foo")); fail("expected BrokenHttpClientException"); } catch (ApiWrapper.BrokenHttpClientException expected) { verify(client, times(1)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } reset(client); when(client.execute(any(HttpHost.class), any(HttpUriRequest.class))).thenThrow(new NullPointerException()); try { broken.execute(new HttpGet("/foo")); fail("expected BrokenHttpClientException"); } catch (ApiWrapper.BrokenHttpClientException expected) { // make sure client retried request verify(client, times(2)).execute(any(HttpHost.class), any(HttpUriRequest.class)); } } @Test public void testAddClientIdWithoutToken() throws Exception { assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo?client_id=" + TEST_CLIENT_ID)); } @Test public void testShouldAlwaysAddClientIdEvenWhenAuthenticated() throws Exception { api.setToken(new Token("access", "refresh")); assertThat(api.addClientIdIfNecessary(Request.to("/foo")).toUrl(), equalTo("/foo?client_id=" + TEST_CLIENT_ID)); } @Test public void testDontAddClientIdIfManuallyAdded() throws Exception { final Request req = Request.to("/foo").with("client_id", "12345"); assertThat(api.addClientIdIfNecessary(req).toUrl(), equalTo("/foo?client_id=12345")); } @Test public void testAddDefaultParameters() throws Exception { layer.addHttpResponseRule("/foo?client_id=" + TEST_CLIENT_ID, "Hi"); layer.addHttpResponseRule("/foo?t=1&client_id=" + TEST_CLIENT_ID, "Hi t1"); layer.addHttpResponseRule("/foo?t=2&client_id=" + TEST_CLIENT_ID, "Hi t2"); final Request foo = Request.to("/foo"); for (int i = 0; i < 1000; i++) { final Exception throwable[] = new Exception[2]; Thread t1 = new Thread("t1") { @Override public void run() { ApiWrapper.setDefaultParameter("t", "1"); try { assertEquals("Hi t1", Http.getString(api.get(foo))); } catch (Exception e) { throwable[0] = e; } ApiWrapper.clearDefaultParameters(); try { assertEquals("Hi", Http.getString(api.get(foo))); } catch (Exception e) { throwable[0] = e; } } }; Thread t2 = new Thread("t2") { @Override public void run() { ApiWrapper.setDefaultParameter("t", "2"); try { assertEquals("Hi t2", Http.getString(api.get(foo))); } catch (Exception e) { throwable[1] = e; } ApiWrapper.clearDefaultParameters(); try { assertEquals("Hi", Http.getString(api.get(foo))); } catch (Exception e) { throwable[1] = e; } } }; t1.start(); t2.start(); t1.join(); t2.join(); if (throwable[0] != null) throw throwable[0]; if (throwable[1] != null) throw throwable[1]; assertEquals("Hi", Http.getString(api.get(foo))); } } }