/**
* Copyright 2012 Facebook
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook;
import android.app.Activity;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.facebook.model.GraphObject;
import com.facebook.internal.Utility;
import junit.framework.AssertionFailedError;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.*;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class FacebookActivityTestCase<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
private static final String TAG = FacebookActivityTestCase.class.getSimpleName();
private static String applicationId;
private static String applicationSecret;
public final static String SECOND_TEST_USER_TAG = "Second";
public final static String THIRD_TEST_USER_TAG = "Third";
private TestBlocker testBlocker;
protected synchronized TestBlocker getTestBlocker() {
if (testBlocker == null) {
testBlocker = TestBlocker.createTestBlocker();
}
return testBlocker;
}
public FacebookActivityTestCase(Class<T> activityClass) {
super("", activityClass);
}
// Returns an un-opened TestSession
protected TestSession getTestSessionWithSharedUser() {
return getTestSessionWithSharedUser(null);
}
// Returns an un-opened TestSession
protected TestSession getTestSessionWithSharedUser(String sessionUniqueUserTag) {
return getTestSessionWithSharedUserAndPermissions(sessionUniqueUserTag, new ArrayList<String>());
}
protected TestSession getTestSessionWithSharedUserAndPermissions(String sessionUniqueUserTag,
List<String> permissions) {
return TestSession.createSessionWithSharedUser(getActivity(), permissions, sessionUniqueUserTag);
}
// Returns an un-opened TestSession
protected TestSession getTestSessionWithPrivateUser(TestBlocker testBlocker) {
return TestSession.createSessionWithPrivateUser(getActivity(), null);
}
protected TestSession openTestSessionWithSharedUser(final TestBlocker blocker) {
return openTestSessionWithSharedUser(blocker, null);
}
protected TestSession openTestSessionWithSharedUser(final TestBlocker blocker, String sessionUniqueUserTag) {
TestSession session = getTestSessionWithSharedUser();
openSession(getActivity(), session, blocker);
return session;
}
protected TestSession openTestSessionWithSharedUser() {
return openTestSessionWithSharedUser((String) null);
}
protected TestSession openTestSessionWithSharedUser(String sessionUniqueUserTag) {
return openTestSessionWithSharedUserAndPermissions(sessionUniqueUserTag, (String[]) null);
}
protected TestSession openTestSessionWithSharedUserAndPermissions(String sessionUniqueUserTag,
String... permissions) {
List<String> permissionList = (permissions != null) ? Arrays.asList(permissions) : null;
return openTestSessionWithSharedUserAndPermissions(sessionUniqueUserTag, permissionList);
}
protected TestSession openTestSessionWithSharedUserAndPermissions(String sessionUniqueUserTag,
List<String> permissions) {
final TestBlocker blocker = getTestBlocker();
TestSession session = getTestSessionWithSharedUserAndPermissions(sessionUniqueUserTag, permissions);
openSession(getActivity(), session, blocker);
return session;
}
// Turns exceptions from the TestBlocker into JUnit assertions
protected void waitAndAssertSuccess(TestBlocker testBlocker, int numSignals) {
try {
testBlocker.waitForSignalsAndAssertSuccess(numSignals);
} catch (AssertionFailedError e) {
throw e;
} catch (Exception e) {
fail("Got exception: " + e.getMessage());
}
}
protected void waitAndAssertSuccess(int numSignals) {
waitAndAssertSuccess(getTestBlocker(), numSignals);
}
protected void waitAndAssertSuccessOrRethrow(int numSignals) throws Exception {
getTestBlocker().waitForSignalsAndAssertSuccess(numSignals);
}
protected void runAndBlockOnUiThread(final int expectedSignals, final Runnable runnable) throws Throwable {
final TestBlocker blocker = getTestBlocker();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
runnable.run();
blocker.signal();
}
});
// We wait for the operation to complete; wait for as many other signals as we expect.
blocker.waitForSignals(1 + expectedSignals);
// Wait for the UI thread to become idle so any UI updates the runnable triggered have a chance
// to finish before we return.
getInstrumentation().waitForIdleSync();
}
protected synchronized void readApplicationIdAndSecret() {
synchronized (FacebookTestCase.class) {
if (applicationId != null && applicationSecret != null) {
return;
}
AssetManager assets = getInstrumentation().getContext().getResources().getAssets();
InputStream stream = null;
final String errorMessage = "could not read applicationId and applicationSecret from config.json; ensure "
+ "you have run 'configure_unit_tests.sh'. Error: ";
try {
stream = assets.open("config.json");
String string = Utility.readStreamToString(stream);
JSONTokener tokener = new JSONTokener(string);
Object obj = tokener.nextValue();
if (!(obj instanceof JSONObject)) {
fail(errorMessage + "could not deserialize a JSONObject");
}
JSONObject jsonObject = (JSONObject) obj;
applicationId = jsonObject.optString("applicationId");
applicationSecret = jsonObject.optString("applicationSecret");
if (Utility.isNullOrEmpty(applicationId) || Utility.isNullOrEmpty(applicationSecret)) {
fail(errorMessage + "one or both config values are missing");
}
TestSession.setTestApplicationId(applicationId);
TestSession.setTestApplicationSecret(applicationSecret);
} catch (IOException e) {
fail(errorMessage + e.toString());
} catch (JSONException e) {
fail(errorMessage + e.toString());
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
fail(errorMessage + e.toString());
}
}
}
}
}
protected void openSession(Activity activity, TestSession session) {
final TestBlocker blocker = getTestBlocker();
openSession(activity, session, blocker);
}
protected void openSession(Activity activity, TestSession session, final TestBlocker blocker) {
Session.OpenRequest openRequest = new Session.OpenRequest(activity).
setCallback(new Session.StatusCallback() {
boolean signaled = false;
@Override
public void call(Session session, SessionState state, Exception exception) {
if (exception != null) {
Log.w(TAG,
"openSession: received an error opening session: " + exception.toString());
}
assertTrue(exception == null);
// Only signal once, or we might screw up the count on the blocker.
if (!signaled) {
blocker.signal();
signaled = true;
}
}
});
session.openForRead(openRequest);
waitAndAssertSuccess(blocker, 1);
}
protected void setUp() throws Exception {
super.setUp();
// Make sure we have read application ID and secret.
readApplicationIdAndSecret();
// These are useful for debugging unit test failures.
Settings.addLoggingBehavior(LoggingBehavior.REQUESTS);
Settings.addLoggingBehavior(LoggingBehavior.INCLUDE_ACCESS_TOKENS);
// We want the UI thread to be in StrictMode to catch any violations.
turnOnStrictModeForUiThread();
}
protected void tearDown() throws Exception {
super.tearDown();
if (testBlocker != null) {
testBlocker.quit();
}
}
protected Bundle getNativeLinkingExtras(String token) {
Bundle extras = new Bundle();
String extraLaunchUriString = String
.format("fbrpc://facebook/nativethirdparty?app_id=%s&package_name=com.facebook.sdk.tests&class_name=com.facebook.FacebookActivityTests$FacebookTestActivity&access_token=%s",
TestSession.getTestApplicationId(), token);
extras.putString("extra_launch_uri", extraLaunchUriString);
extras.putString("expires_in", "3600");
extras.putLong("app_id", Long.parseLong(TestSession.getTestApplicationId()));
extras.putString("access_token", token);
return extras;
}
interface GraphObjectPostResult extends GraphObject {
String getId();
}
protected GraphObject getAndAssert(Session session, String id) {
Request request = new Request(session, id);
Response response = request.executeAndWait();
assertNotNull(response);
assertNull(response.getError());
GraphObject result = response.getGraphObject();
assertNotNull(result);
return result;
}
protected GraphObject postGetAndAssert(Session session, String path, GraphObject graphObject) {
Request request = Request.newPostRequest(session, path, graphObject, null);
Response response = request.executeAndWait();
assertNotNull(response);
assertNull(response.getError());
GraphObjectPostResult result = response.getGraphObjectAs(GraphObjectPostResult.class);
assertNotNull(result);
assertNotNull(result.getId());
return getAndAssert(session, result.getId());
}
protected void setBatchApplicationIdForTestApp() {
String appId = TestSession.getTestApplicationId();
Request.setDefaultBatchApplicationId(appId);
}
protected <U extends GraphObject> U batchCreateAndGet(Session session, String graphPath, GraphObject graphObject,
String fields, Class<U> resultClass) {
Request create = Request.newPostRequest(session, graphPath, graphObject, new ExpectSuccessCallback());
create.setBatchEntryName("create");
Request get = Request.newGraphPathRequest(session, "{result=create:$.id}", new ExpectSuccessCallback());
if (fields != null) {
Bundle parameters = new Bundle();
parameters.putString("fields", fields);
get.setParameters(parameters);
}
return batchPostAndGet(create, get, resultClass);
}
protected <U extends GraphObject> U batchUpdateAndGet(Session session, String graphPath, GraphObject graphObject,
String fields, Class<U> resultClass) {
Request update = Request.newPostRequest(session, graphPath, graphObject, new ExpectSuccessCallback());
Request get = Request.newGraphPathRequest(session, graphPath, new ExpectSuccessCallback());
if (fields != null) {
Bundle parameters = new Bundle();
parameters.putString("fields", fields);
get.setParameters(parameters);
}
return batchPostAndGet(update, get, resultClass);
}
protected <U extends GraphObject> U batchPostAndGet(Request post, Request get, Class<U> resultClass) {
List<Response> responses = Request.executeBatchAndWait(post, get);
assertEquals(2, responses.size());
U resultGraphObject = responses.get(1).getGraphObjectAs(resultClass);
assertNotNull(resultGraphObject);
return resultGraphObject;
}
protected GraphObject createStatusUpdate() {
GraphObject statusUpdate = GraphObject.Factory.create();
String message = String.format(
"Check out my awesome new status update posted at: %s. Some chars for you: +\"[]:,", new Date());
statusUpdate.setProperty("message", message);
return statusUpdate;
}
protected Bitmap createTestBitmap(int size) {
Bitmap image = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565);
image.eraseColor(Color.BLUE);
return image;
}
protected void issueFriendRequest(TestSession session, String targetUserId) {
String graphPath = "me/friends/" + targetUserId;
Request request = Request.newPostRequest(session, graphPath, null, null);
Response response = request.executeAndWait();
// We will get a 400 error if the users are already friends.
FacebookRequestError error = response.getError();
assertTrue(error == null || error.getRequestStatusCode() == 400);
}
protected void makeTestUsersFriends(TestSession session1, TestSession session2) {
issueFriendRequest(session1, session2.getTestUserId());
issueFriendRequest(session2, session1.getTestUserId());
}
protected void assertDateEqualsWithinDelta(Date expected, Date actual, long deltaInMsec) {
long delta = Math.abs(expected.getTime() - actual.getTime());
assertTrue(delta < deltaInMsec);
}
protected void assertDateDiffersWithinDelta(Date expected, Date actual, long expectedDifference, long deltaInMsec) {
long delta = Math.abs(expected.getTime() - actual.getTime()) - expectedDifference;
assertTrue(delta < deltaInMsec);
}
protected void assertNoErrors(List<Response> responses) {
for (int i = 0; i < responses.size(); ++i) {
Response response = responses.get(i);
assertNotNull(response);
assertNull(response.getError());
}
}
protected File createTempFileFromAsset(String assetPath) throws IOException {
InputStream inputStream = null;
FileOutputStream outStream = null;
try {
AssetManager assets = getInstrumentation().getContext().getResources().getAssets();
inputStream = assets.open(assetPath);
File outputDir = getActivity().getCacheDir(); // context being the Activity pointer
File outputFile = File.createTempFile("prefix", assetPath, outputDir);
outStream = new FileOutputStream(outputFile);
final int bufferSize = 1024 * 2;
byte[] buffer = new byte[bufferSize];
int n = 0;
while ((n = inputStream.read(buffer)) != -1) {
outStream.write(buffer, 0, n);
}
return outputFile;
} finally {
Utility.closeQuietly(outStream);
Utility.closeQuietly(inputStream);
}
}
protected void runOnBlockerThread(final Runnable runnable, boolean waitForCompletion) {
Runnable runnableToPost = runnable;
final ConditionVariable condition = waitForCompletion ? new ConditionVariable(!waitForCompletion) : null;
if (waitForCompletion) {
runnableToPost = new Runnable() {
@Override
public void run() {
runnable.run();
condition.open();
}
};
}
TestBlocker blocker = getTestBlocker();
Handler handler = blocker.getHandler();
handler.post(runnableToPost);
if (waitForCompletion) {
boolean success = condition.block(10000);
assertTrue(success);
}
}
protected void closeBlockerAndAssertSuccess() {
TestBlocker blocker = getTestBlocker();
testBlocker = null;
blocker.quit();
boolean joined = false;
while (!joined) {
try {
blocker.join();
joined = true;
} catch (InterruptedException e) {
}
}
try {
blocker.assertSuccess();
} catch (Exception e) {
fail(e.toString());
}
}
protected TestRequestAsyncTask createAsyncTaskOnUiThread(final Request... requests) throws Throwable {
final ArrayList<TestRequestAsyncTask> result = new ArrayList<TestRequestAsyncTask>();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
result.add(new TestRequestAsyncTask(requests));
}
});
return result.isEmpty() ? null : result.get(0);
}
/*
* Classes and helpers related to asynchronous requests.
*/
// A subclass of RequestAsyncTask that knows how to interact with TestBlocker to ensure that tests can wait
// on and assert success of async tasks.
protected class TestRequestAsyncTask extends RequestAsyncTask {
private final TestBlocker blocker = FacebookActivityTestCase.this.getTestBlocker();
public TestRequestAsyncTask(Request... requests) {
super(requests);
}
public TestRequestAsyncTask(List<Request> requests) {
super(requests);
}
public TestRequestAsyncTask(RequestBatch requests) {
super(requests);
}
public TestRequestAsyncTask(HttpURLConnection connection, Request... requests) {
super(connection, requests);
}
public TestRequestAsyncTask(HttpURLConnection connection, List<Request> requests) {
super(connection, requests);
}
public TestRequestAsyncTask(HttpURLConnection connection, RequestBatch requests) {
super(connection, requests);
}
public final TestBlocker getBlocker() {
return blocker;
}
public final Exception getThrowable() {
return getException();
}
protected void onPostExecute(List<Response> result) {
try {
super.onPostExecute(result);
if (getException() != null) {
blocker.setException(getException());
}
} finally {
Log.d("TestRequestAsyncTask", "signaling blocker");
blocker.signal();
}
}
// In order to be able to block and accumulate exceptions, we want to ensure the async task is really
// being started on the blocker's thread, rather than the test's thread. Use this instead of calling
// execute directly in unit tests.
public void executeOnBlockerThread() {
ensureAsyncTaskLoaded();
Runnable runnable = new Runnable() {
public void run() {
execute();
}
};
Handler handler = new Handler(blocker.getLooper());
handler.post(runnable);
}
private void ensureAsyncTaskLoaded() {
// Work around this issue on earlier frameworks: http://stackoverflow.com/a/7818839/782044
try {
runAndBlockOnUiThread(0, new Runnable() {
@Override
public void run() {
try {
Class.forName("android.os.AsyncTask");
} catch (ClassNotFoundException e) {
}
}
});
} catch (Throwable throwable) {
}
}
}
// Provides an implementation of Request.Callback that will assert either success (no error) or failure (error)
// of a request, and allow derived classes to perform additional asserts.
protected class TestCallback implements Request.Callback {
private final TestBlocker blocker;
private final boolean expectSuccess;
public TestCallback(TestBlocker blocker, boolean expectSuccess) {
this.blocker = blocker;
this.expectSuccess = expectSuccess;
}
public TestCallback(boolean expectSuccess) {
this(FacebookActivityTestCase.this.getTestBlocker(), expectSuccess);
}
@Override
public void onCompleted(Response response) {
try {
// We expect to be called on the right thread.
if (Thread.currentThread() != blocker) {
throw new FacebookException("Invalid thread " + Thread.currentThread().getId()
+ "; expected to be called on thread " + blocker.getId());
}
// We expect either success or failure.
if (expectSuccess && response.getError() != null) {
throw response.getError().getException();
} else if (!expectSuccess && response.getError() == null) {
throw new FacebookException("Expected failure case, received no error");
}
// Some tests may want more fine-grained control and assert additional conditions.
performAsserts(response);
} catch (Exception e) {
blocker.setException(e);
} finally {
// Tell anyone waiting on us that this callback was called.
blocker.signal();
}
}
protected void performAsserts(Response response) {
}
}
// A callback that will assert if the request resulted in an error.
protected class ExpectSuccessCallback extends TestCallback {
public ExpectSuccessCallback() {
super(true);
}
}
// A callback that will assert if the request did NOT result in an error.
protected class ExpectFailureCallback extends TestCallback {
public ExpectFailureCallback() {
super(false);
}
}
public static abstract class MockRequest extends Request {
public abstract Response createResponse();
}
public static class MockRequestBatch extends RequestBatch {
public MockRequestBatch(MockRequest... requests) {
super(requests);
}
// Caller must ensure that all the requests in the batch are, in fact, MockRequests.
public MockRequestBatch(RequestBatch requests) {
super(requests);
}
@Override
List<Response> executeAndWaitImpl() {
List<Request> requests = getRequests();
List<Response> responses = new ArrayList<Response>();
for (Request request : requests) {
MockRequest mockRequest = (MockRequest) request;
responses.add(mockRequest.createResponse());
}
Request.runCallbacks(this, responses);
return responses;
}
}
private AtomicBoolean strictModeOnForUiThread = new AtomicBoolean();
protected void turnOnStrictModeForUiThread() {
// We only ever need to do this once. If the boolean is true, we know that the next runnable
// posted to the UI thread will have strict mode on.
if (strictModeOnForUiThread.get() == false) {
try {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
// Double-check whether we really need to still do this on the UI thread.
if (strictModeOnForUiThread.compareAndSet(false, true)) {
turnOnStrictModeForThisThread();
}
}
});
} catch (Throwable throwable) {
}
}
}
protected void turnOnStrictModeForThisThread() {
// We use reflection, because Instrumentation will complain about any references to StrictMode in API versions < 9
// when attempting to run the unit tests. No particular effort has been made to make this efficient, since we
// expect to call it just once.
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> strictModeClass = Class.forName("android.os.StrictMode", true, loader);
Class<?> threadPolicyClass = Class.forName("android.os.StrictMode$ThreadPolicy", true, loader);
Class<?> threadPolicyBuilderClass = Class.forName("android.os.StrictMode$ThreadPolicy$Builder", true,
loader);
Object threadPolicyBuilder = threadPolicyBuilderClass.getConstructor().newInstance();
threadPolicyBuilder = threadPolicyBuilderClass.getMethod("detectAll").invoke(threadPolicyBuilder);
threadPolicyBuilder = threadPolicyBuilderClass.getMethod("penaltyDeath").invoke(threadPolicyBuilder);
Object threadPolicy = threadPolicyBuilderClass.getMethod("build").invoke(threadPolicyBuilder);
strictModeClass.getMethod("setThreadPolicy", threadPolicyClass).invoke(strictModeClass, threadPolicy);
} catch (Exception ex) {
}
}
}