package com.soundcloud.api;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CloudAPIIntegrationTest implements Params.Track, Endpoints {
// https://soundcloud.com/you/apps/java-api-wrapper
// user: api-testing
static final String CLIENT_ID = "40d3111c6b4d02096c6ce35fdf90bf58";
static final String CLIENT_SECRET = "ff3685dbf02ce789a16631b0028e0512";
public static final String TRACK_PERMALINK = "http://soundcloud.com/jberkel/nobody-home";
public static final String MEDIA_LINK = "http://media.soundcloud.com/stream/zkwlN5MGNsJt";
public static final long USER_ID = 18173653L;
public static final long CHE_FLUTE_TRACK_ID = 274334;
public static final long FLICKERMOOD_TRACK_ID = 293;
public static final long TRACK_LENGTH = 224861L;
CloudAPI api;
static final String USERNAME = "android-testing";
static final String PASSWORD = "android-testing";
/*
To get full HTTP logging, add the following system properties:
-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
-Dorg.apache.commons.logging.simplelog.showdatetime=true
-Dorg.apache.commons.logging.simplelog.log.org.apache.http=DEBUG
-Dorg.apache.commons.logging.simplelog.log.org.apache.http.wire=ERROR
*/
@Rule
public Retry retryRule = new Retry(3);
@Before
public void setUp() throws Exception {
api = new ApiWrapper(
CLIENT_ID,
CLIENT_SECRET,
null,
null);
}
private Token login(String... scopes) throws IOException {
return api.login(USERNAME, PASSWORD, scopes);
}
@Test
public void shouldBeAbleToMakePublicRequests() throws Exception {
HttpResponse response = api.get(Request.to("/tracks").with("order", "hotness"));
assertEquals(200, response.getStatusLine().getStatusCode());
}
@Test
public void shouldUploadASimpleAudioFile() throws Exception {
login();
HttpResponse resp = api.post(Request.to(TRACKS).with(
TITLE, "Hello Android",
POST_TO_EMPTY, "")
.withFile(ASSET_DATA, new File(getClass().getResource("hello.aiff").getFile())));
int status = resp.getStatusLine().getStatusCode();
assertThat(status, is(201));
Header location = resp.getFirstHeader("Location");
assertNotNull(location);
}
@Test
public void shouldCreateAPlaylistAndAddTracksToIt() throws Exception {
login();
HttpResponse resp = api.post(Request.to(PLAYLISTS)
.with("playlist[title]", "test playlist"));
int status = resp.getStatusLine().getStatusCode();
assertThat(status, is(201));
Header location = resp.getFirstHeader("Location");
assertNotNull(location);
String playlistUrl = location.getValue();
assertNotNull(playlistUrl);
String title = "a new title:" + System.currentTimeMillis();
resp = api.put(Request.to(playlistUrl)
.with("playlist[title]", title)
.with("playlist[tracks][][id]", CHE_FLUTE_TRACK_ID)
.with("playlist[tracks][][id]", FLICKERMOOD_TRACK_ID));
status = resp.getStatusLine().getStatusCode();
assertThat(status, is(200));
JSONObject obj = new JSONObject(EntityUtils.toString(resp.getEntity()));
assertThat(obj.getString("kind"), equalTo("playlist"));
assertThat(obj.getString("title"), equalTo(title));
assertThat(obj.getInt("track_count"), equalTo(2));
}
@Test
public void shouldCreateAPlaylistAndAddTracksToItWithJSON() throws Exception {
login();
HttpResponse resp = api.post(Request.to(PLAYLISTS)
.with("playlist[title]", "test playlist"));
int status = resp.getStatusLine().getStatusCode();
assertThat(status, is(201));
Header location = resp.getFirstHeader("Location");
assertNotNull(location);
String playlistUrl = location.getValue();
assertNotNull(playlistUrl);
String title = "a new tîtle:" + System.currentTimeMillis();
JSONObject json = createJSONPlaylist(title, CHE_FLUTE_TRACK_ID, FLICKERMOOD_TRACK_ID);
resp = api.put(Request.to(playlistUrl)
.withContent(json.toString(), "application/json"));
status = resp.getStatusLine().getStatusCode();
assertThat(status, is(200));
JSONObject obj = new JSONObject(EntityUtils.toString(resp.getEntity()));
assertThat(obj.getString("kind"), equalTo("playlist"));
assertThat(obj.getString("title"), equalTo(title));
assertThat(obj.getInt("track_count"), equalTo(2));
}
@Test
public void shouldUploadASimpleAudioFileBytes() throws Exception {
login();
File f = new File(getClass().getResource("hello.aiff").getFile());
ByteBuffer bb = ByteBuffer.allocate((int) f.length());
FileInputStream fis = new FileInputStream(f);
for (;;) if (fis.getChannel().read(bb) <= 0) break;
HttpResponse resp = api.post(Request.to(TRACKS).with(
TITLE, "Hello Android",
POST_TO_EMPTY, "")
.withFile(ASSET_DATA, bb, "hello.aiff"));
int status = resp.getStatusLine().getStatusCode();
assertThat(status, is(201));
}
@Test(expected = IOException.class) @Ignore
public void shouldNotGetASignupTokenWhenInofficialApp() throws Exception {
login();
api.clientCredentials();
}
@Test(expected = CloudAPI.InvalidTokenException.class)
public void shouldGetATokenUsingExtensionGrantTypes() throws Exception {
// TODO ?
api.extensionGrantType(CloudAPI.FACEBOOK_GRANT_TYPE + "fbToken");
}
@Test
public void shouldReturn401WithInvalidToken() throws Exception {
login();
api.setToken(new Token("invalid", "invalid"));
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(401));
}
@Test
public void shouldWorkWithRelativeUrls() throws Exception {
login();
HttpResponse resp = api.get(Request.to("me"));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
}
@Test
public void shouldRefreshAutomaticallyWhenTokenExpired() throws Exception {
login();
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
final Token oldToken = api.getToken();
assertThat(api.invalidateToken(), is(nullValue()));
resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
// make sure we've got a new token
assertThat(oldToken, not(equalTo(api.getToken())));
}
@Test
public void shouldResolveUrls() throws Exception {
login();
long id = api.resolve("http://soundcloud.com/" + USERNAME);
assertThat(id, is(USER_ID));
try {
id = api.resolve("http://soundcloud.com/i-do-no-exist-no-no-no");
fail("expected resolver exception, got: "+id);
} catch (CloudAPI.ResolverException e) {
// expected
assertThat(e.getStatusCode(), is(404));
}
}
@Test
public void shouldResolveStreamUrls() throws Exception {
login();
String streamUrl = getApiUrlFromPermalink(TRACK_PERMALINK) + "/stream";
Stream resolved = api.resolveStreamUrl(streamUrl, false);
assertThat(resolved.url, equalTo(streamUrl));
assertThat(resolved.streamUrl, containsString("https://ec-media.soundcloud.com/"));
assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis());
assertThat(resolved.eTag, equalTo("\"980f61d6d6ee26ffe0c78aef618d786f\""));
}
@Test
public void shouldResolveNonApiStreamUrls() throws Exception {
testResolveNonApiStreamUrls();
}
@Test
public void shouldResolveNonApiStreamUrlsWithLogin() throws Exception {
login();
testResolveNonApiStreamUrls();
}
private void testResolveNonApiStreamUrls() throws IOException {
Stream resolved = api.resolveStreamUrl(MEDIA_LINK, false);
assertThat(resolved.url, equalTo(MEDIA_LINK));
assertThat(resolved.streamUrl, containsString("http://ec-media.soundcloud.com/"));
assertTrue("expire should be in the future", resolved.expires > System.currentTimeMillis());
assertThat(resolved.eTag, equalTo("\"5eeb63b73f99ff2de44a60441d421d2a\""));
}
@Test
public void shouldThrowResolverExceptionWhenStreamCannotBeResolved() throws Exception {
login();
try {
Stream s = api.resolveStreamUrl("https://api.soundcloud.com/tracks/999919191/stream", false);
fail("expected resolver exception, got: "+s);
} catch (CloudAPI.ResolverException e) {
// expected
assertThat(e.getStatusCode(), is(404));
}
}
@Test
public void shouldSupportRangeRequest() throws Exception {
login();
String streamUrl = getApiUrlFromPermalink(TRACK_PERMALINK)+"/stream";
Stream resolved = api.resolveStreamUrl(streamUrl, false);
assertThat(resolved.contentLength, is(TRACK_LENGTH));
HttpResponse resp = api
.getHttpClient()
.execute(resolved.streamUrl().range(50, 100).buildRequest(HttpGet.class));
assertThat(resp.getStatusLine().toString(), resp.getStatusLine().getStatusCode(), is(206));
Header range = resp.getFirstHeader("Content-Range");
assertThat(range, notNullValue());
assertThat(range.getValue(), equalTo("bytes 50-100/"+TRACK_LENGTH));
assertThat(resp.getEntity().getContentLength(), is(51L));
}
@Test
public void readMyDetails() throws Exception {
login();
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
assertThat(
resp.getFirstHeader("Content-Type").getValue(),
containsString("application/json"));
JSONObject me = Http.getJSON(resp);
assertThat(me.getString("username"), equalTo(USERNAME));
// writeResponse(resp, "me.json");
}
@Test
public void readGzipCompressedData() throws Exception {
api.setDefaultAcceptEncoding("gzip");
login();
HttpResponse resp = api.get(Request.to(Endpoints.TRACKS));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
assertThat(
resp.getFirstHeader("Content-Type").getValue(),
containsString("application/json"));
assertThat(
resp.getFirstHeader("Content-Encoding").getValue(),
equalTo("gzip"));
JSONArray array = new JSONArray(new JSONTokener(new InputStreamReader(resp.getEntity().getContent())));
assertTrue("array is empty", array.length() > 0);
}
@Test
public void shouldLoginWithNonExpiringScope() throws Exception {
Token token = login(Token.SCOPE_NON_EXPIRING);
assertThat(token.scoped(Token.SCOPE_NON_EXPIRING), is(true));
assertThat(token.refresh, is(nullValue()));
assertThat(token.getExpiresIn(), is(nullValue()));
assertThat(token.valid(), is(true));
// make sure we can issue a request with this token
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(200));
}
@Test
public void shouldNotRefreshWithNonExpiringScope() throws Exception {
Token token = login(Token.SCOPE_NON_EXPIRING);
assertThat(token.scoped(Token.SCOPE_NON_EXPIRING), is(true));
assertThat(api.invalidateToken(), is(nullValue()));
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(401));
}
@Test
public void shouldChangeContentType() throws Exception {
login();
api.setDefaultContentType("application/xml");
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(
resp.getFirstHeader("Content-Type").getValue(),
containsString("application/xml"));
}
@Test
public void shouldSupportConditionalGets() throws Exception {
login();
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
assertThat(resp.getStatusLine().getStatusCode(), is(200) /* ok */);
String etag = Http.etag(resp);
assertNotNull(etag);
resp = api.get(Request.to(Endpoints.MY_DETAILS).ifNoneMatch(etag));
assertThat(resp.getStatusLine().getStatusCode(), is(304) /* not-modified */);
}
@Test(expected = UnknownHostException.class)
public void shouldRespectProxySettings() throws Exception {
System.setProperty("http.proxyHost", "http://doesnotexist.example.com");
try {
login();
} finally {
System.clearProperty("http.proxyHost");
}
}
@Test @Ignore
public void shouldSupportConcurrentConnectionsToApiHost() throws Exception {
login();
int num = 20;
final CyclicBarrier start = new CyclicBarrier(num, new Runnable() {
@Override
public void run() {
System.err.println("starting...");
}
});
final CyclicBarrier end = new CyclicBarrier(num);
while (num-- > 0) {
new Thread("t-"+num) {
@Override public void run() {
try {
start.await();
System.err.println("running: "+toString());
try {
HttpResponse resp = api.get(Request.to(Endpoints.MY_DETAILS));
resp.getEntity().consumeContent();
assertThat(resp.getStatusLine().getStatusCode(), is(200));
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.err.println("finished: "+toString());
end.await();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}.start();
}
start.await();
end.await();
System.err.println("all threads finished");
}
@SuppressWarnings({"UnusedDeclaration"})
private void writeResponse(HttpResponse resp, String file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
InputStream is = resp.getEntity().getContent();
byte[] b = new byte[8192];
int n;
while ((n = is.read(b)) >= 0) fos.write(b, 0, n);
is.close();
fos.close();
}
private String getApiUrlFromPermalink(String permalink) throws IOException {
long trackId = api.resolve(permalink);
return "https://api.soundcloud.com/tracks/" + trackId;
}
private JSONObject createJSONPlaylist(String title, long... trackIds) throws JSONException {
JSONObject playlist = new JSONObject();
playlist.put("title", title);
JSONObject json = new JSONObject();
json.put("playlist", playlist);
JSONArray tracks = new JSONArray();
playlist.put("tracks", tracks);
for (long id : trackIds) {
JSONObject track = new JSONObject();
track.put("id", id);
tracks.put(track);
}
return json;
}
// We had trouble with some tests randomly failing, perhaps due to replication lag
// see http://stackoverflow.com/questions/8295100/how-to-re-run-failed-junit-tests-immediately
public static class Retry implements TestRule {
private int retryCount;
public Retry(int retryCount) {
this.retryCount = retryCount;
}
public Statement apply(Statement base, Description description) {
return statement(base, description);
}
private Statement statement(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Throwable caughtThrowable = null;
// implement retry logic here
for (int i = 0; i < retryCount; i++) {
try {
base.evaluate();
return;
} catch (Throwable t) {
caughtThrowable = t;
System.err.println(description.getDisplayName() + ": run " + (i+1) + " failed");
}
}
System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures");
throw caughtThrowable;
}
};
}
}
}