/facebook/src/com/facebook/TestSession.java
Java | 516 lines | 294 code | 88 blank | 134 comment | 51 complexity | c731f01a46c4e58bfdec3c86b33a8203 MD5 | raw file
- /**
- * Copyright 2010-present 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.os.Bundle;
- import android.text.TextUtils;
- import android.util.Log;
- import com.facebook.model.GraphObject;
- import com.facebook.model.GraphObjectList;
- import com.facebook.internal.Logger;
- import com.facebook.internal.Utility;
- import com.facebook.internal.Validate;
- import org.json.JSONException;
- import org.json.JSONObject;
- import java.util.*;
- /**
- * Implements an subclass of Session that knows about test users for a particular
- * application. This should never be used from a real application, but may be useful
- * for writing unit tests, etc.
- * <p/>
- * Facebook allows developers to create test accounts for testing their applications'
- * Facebook integration (see https://developers.facebook.com/docs/test_users/). This class
- * simplifies use of these accounts for writing unit tests. It is not designed for use in
- * production application code.
- * <p/>
- * The main use case for this class is using {@link #createSessionWithPrivateUser(android.app.Activity, java.util.List)}
- * or {@link #createSessionWithSharedUser(android.app.Activity, java.util.List)}
- * to create a session for a test user. Two modes are supported. In "shared" mode, an attempt
- * is made to find an existing test user that has the required permissions. If no such user is available,
- * a new one is created with the required permissions. In "private" mode, designed for
- * scenarios which require a new user in a known clean state, a new test user will always be
- * created, and it will be automatically deleted when the TestSession is closed. The session
- * obeys the same lifecycle as a regular Session, meaning it must be opened after creation before
- * it can be used to make calls to the Facebook API.
- * <p/>
- * Prior to creating a TestSession, two static methods must be called to initialize the
- * application ID and application Secret to be used for managing test users. These methods are
- * {@link #setTestApplicationId(String)} and {@link #setTestApplicationSecret(String)}.
- * <p/>
- * Note that the shared test user functionality depends on a naming convention for the test users.
- * It is important that any testing of functionality which will mutate the permissions for a
- * test user NOT use a shared test user, or this scheme will break down. If a shared test user
- * seems to be in an invalid state, it can be deleted manually via the Web interface at
- * https://developers.facebook.com/apps/APP_ID/permissions?role=test+users.
- */
- public class TestSession extends Session {
- private static final long serialVersionUID = 1L;
- private enum Mode {
- PRIVATE, SHARED
- }
- private static final String LOG_TAG = Logger.LOG_TAG_BASE + "TestSession";
- private static Map<String, TestAccount> appTestAccounts;
- private static String testApplicationSecret;
- private static String testApplicationId;
- private final String sessionUniqueUserTag;
- private final List<String> requestedPermissions;
- private final Mode mode;
- private String testAccountId;
- private boolean wasAskedToExtendAccessToken;
- TestSession(Activity activity, List<String> permissions, TokenCachingStrategy tokenCachingStrategy,
- String sessionUniqueUserTag, Mode mode) {
- super(activity, TestSession.testApplicationId, tokenCachingStrategy);
- Validate.notNull(permissions, "permissions");
- // Validate these as if they were arguments even though they are statics.
- Validate.notNullOrEmpty(testApplicationId, "testApplicationId");
- Validate.notNullOrEmpty(testApplicationSecret, "testApplicationSecret");
- this.sessionUniqueUserTag = sessionUniqueUserTag;
- this.mode = mode;
- this.requestedPermissions = permissions;
- }
- /**
- * Constructs a TestSession which creates a test user on open, and destroys the user on
- * close; This method should not be used in application code -- but is useful for creating unit tests
- * that use the Facebook SDK.
- *
- * @param activity the Activity to use for opening the session
- * @param permissions list of strings containing permissions to request; nil will result in
- * a common set of permissions (email, publish_actions) being requested
- * @return a new TestSession that is in the CREATED state, ready to be opened
- */
- public static TestSession createSessionWithPrivateUser(Activity activity, List<String> permissions) {
- return createTestSession(activity, permissions, Mode.PRIVATE, null);
- }
- /**
- * Constructs a TestSession which uses a shared test user with the right permissions,
- * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
- * tests).
- * <p/>
- * This method should not be used in application code -- but is useful for creating unit tests
- * that use the Facebook SDK.
- *
- * @param activity the Activity to use for opening the session
- * @param permissions list of strings containing permissions to request; nil will result in
- * a common set of permissions (email, publish_actions) being requested
- * @return a new TestSession that is in the CREATED state, ready to be opened
- */
- public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions) {
- return createSessionWithSharedUser(activity, permissions, null);
- }
- /**
- * Constructs a TestSession which uses a shared test user with the right permissions,
- * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
- * tests).
- * <p/>
- * This method should not be used in application code -- but is useful for creating unit tests
- * that use the Facebook SDK.
- *
- * @param activity the Activity to use for opening the session
- * @param permissions list of strings containing permissions to request; nil will result in
- * a common set of permissions (email, publish_actions) being requested
- * @param sessionUniqueUserTag a string which will be used to make this user unique among other
- * users with the same permissions. Useful for tests which require two or more users to interact
- * with each other, and which therefore must have sessions associated with different users.
- * @return a new TestSession that is in the CREATED state, ready to be opened
- */
- public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions,
- String sessionUniqueUserTag) {
- return createTestSession(activity, permissions, Mode.SHARED, sessionUniqueUserTag);
- }
- /**
- * Gets the Facebook Application ID for the application under test.
- *
- * @return the application ID
- */
- public static synchronized String getTestApplicationId() {
- return testApplicationId;
- }
- /**
- * Sets the Facebook Application ID for the application under test. This must be specified
- * prior to creating a TestSession.
- *
- * @param applicationId the application ID
- */
- public static synchronized void setTestApplicationId(String applicationId) {
- if (testApplicationId != null && !testApplicationId.equals(applicationId)) {
- throw new FacebookException("Can't have more than one test application ID");
- }
- testApplicationId = applicationId;
- }
- /**
- * Gets the Facebook Application Secret for the application under test.
- *
- * @return the application secret
- */
- public static synchronized String getTestApplicationSecret() {
- return testApplicationSecret;
- }
- /**
- * Sets the Facebook Application Secret for the application under test. This must be specified
- * prior to creating a TestSession.
- *
- * @param applicationSecret the application secret
- */
- public static synchronized void setTestApplicationSecret(String applicationSecret) {
- if (testApplicationSecret != null && !testApplicationSecret.equals(applicationSecret)) {
- throw new FacebookException("Can't have more than one test application secret");
- }
- testApplicationSecret = applicationSecret;
- }
- /**
- * Gets the ID of the test user that this TestSession is authenticated as.
- *
- * @return the Facebook user ID of the test user
- */
- public final String getTestUserId() {
- return testAccountId;
- }
- private static synchronized TestSession createTestSession(Activity activity, List<String> permissions, Mode mode,
- String sessionUniqueUserTag) {
- if (Utility.isNullOrEmpty(testApplicationId) || Utility.isNullOrEmpty(testApplicationSecret)) {
- throw new FacebookException("Must provide app ID and secret");
- }
- if (Utility.isNullOrEmpty(permissions)) {
- permissions = Arrays.asList("email", "publish_actions");
- }
- return new TestSession(activity, permissions, new TestTokenCachingStrategy(), sessionUniqueUserTag,
- mode);
- }
- private static synchronized void retrieveTestAccountsForAppIfNeeded() {
- if (appTestAccounts != null) {
- return;
- }
- appTestAccounts = new HashMap<String, TestAccount>();
- // The data we need is split across two different FQL tables. We construct two queries, submit them
- // together (the second one refers to the first one), then cross-reference the results.
- // Get the test accounts for this app.
- String testAccountQuery = String.format("SELECT id,access_token FROM test_account WHERE app_id = %s",
- testApplicationId);
- // Get the user names for those accounts.
- String userQuery = "SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)";
- Bundle parameters = new Bundle();
- // Build a JSON string that contains our queries and pass it as the 'q' parameter of the query.
- JSONObject multiquery;
- try {
- multiquery = new JSONObject();
- multiquery.put("test_accounts", testAccountQuery);
- multiquery.put("users", userQuery);
- } catch (JSONException exception) {
- throw new FacebookException(exception);
- }
- parameters.putString("q", multiquery.toString());
- // We need to authenticate as this app.
- parameters.putString("access_token", getAppAccessToken());
- Request request = new Request(null, "fql", parameters, null);
- Response response = request.executeAndWait();
- if (response.getError() != null) {
- throw response.getError().getException();
- }
- FqlResponse fqlResponse = response.getGraphObjectAs(FqlResponse.class);
- GraphObjectList<FqlResult> fqlResults = fqlResponse.getData();
- if (fqlResults == null || fqlResults.size() != 2) {
- throw new FacebookException("Unexpected number of results from FQL query");
- }
- // We get back two sets of results. The first is from the test_accounts query, the second from the users query.
- Collection<TestAccount> testAccounts = fqlResults.get(0).getFqlResultSet().castToListOf(TestAccount.class);
- Collection<UserAccount> userAccounts = fqlResults.get(1).getFqlResultSet().castToListOf(UserAccount.class);
- // Use both sets of results to populate our static array of accounts.
- populateTestAccounts(testAccounts, userAccounts);
- return;
- }
- private static synchronized void populateTestAccounts(Collection<TestAccount> testAccounts,
- Collection<UserAccount> userAccounts) {
- // We get different sets of data from each of these queries. We want to combine them into a single data
- // structure. We have added a Name property to the TestAccount interface, even though we don't really get
- // a name back from the service from that query. We stick the Name from the corresponding UserAccount in it.
- for (TestAccount testAccount : testAccounts) {
- storeTestAccount(testAccount);
- }
- for (UserAccount userAccount : userAccounts) {
- TestAccount testAccount = appTestAccounts.get(userAccount.getUid());
- if (testAccount != null) {
- testAccount.setName(userAccount.getName());
- }
- }
- }
- private static synchronized void storeTestAccount(TestAccount testAccount) {
- appTestAccounts.put(testAccount.getId(), testAccount);
- }
- private static synchronized TestAccount findTestAccountMatchingIdentifier(String identifier) {
- retrieveTestAccountsForAppIfNeeded();
- for (TestAccount testAccount : appTestAccounts.values()) {
- if (testAccount.getName().contains(identifier)) {
- return testAccount;
- }
- }
- return null;
- }
- @Override
- public final String toString() {
- String superString = super.toString();
- return new StringBuilder().append("{TestSession").append(" testUserId:").append(testAccountId)
- .append(" ").append(superString).append("}").toString();
- }
- @Override
- void authorize(AuthorizationRequest request) {
- if (mode == Mode.PRIVATE) {
- createTestAccountAndFinishAuth();
- } else {
- findOrCreateSharedTestAccount();
- }
- }
- @Override
- void postStateChange(final SessionState oldState, final SessionState newState, final Exception error) {
- // Make sure this doesn't get overwritten.
- String id = testAccountId;
- super.postStateChange(oldState, newState, error);
- if (newState.isClosed() && id != null && mode == Mode.PRIVATE) {
- deleteTestAccount(id, getAppAccessToken());
- }
- }
- boolean getWasAskedToExtendAccessToken() {
- return wasAskedToExtendAccessToken;
- }
- void forceExtendAccessToken(boolean forceExtendAccessToken) {
- AccessToken currentToken = getTokenInfo();
- setTokenInfo(
- new AccessToken(currentToken.getToken(), new Date(), currentToken.getPermissions(),
- AccessTokenSource.TEST_USER, new Date(0)));
- setLastAttemptedTokenExtendDate(new Date(0));
- }
- @Override
- boolean shouldExtendAccessToken() {
- boolean result = super.shouldExtendAccessToken();
- wasAskedToExtendAccessToken = false;
- return result;
- }
- @Override
- void extendAccessToken() {
- wasAskedToExtendAccessToken = true;
- super.extendAccessToken();
- }
- void fakeTokenRefreshAttempt() {
- setCurrentTokenRefreshRequest(new TokenRefreshRequest());
- }
- static final String getAppAccessToken() {
- return testApplicationId + "|" + testApplicationSecret;
- }
- private void findOrCreateSharedTestAccount() {
- TestAccount testAccount = findTestAccountMatchingIdentifier(getSharedTestAccountIdentifier());
- if (testAccount != null) {
- finishAuthWithTestAccount(testAccount);
- } else {
- createTestAccountAndFinishAuth();
- }
- }
- private void finishAuthWithTestAccount(TestAccount testAccount) {
- testAccountId = testAccount.getId();
- AccessToken accessToken = AccessToken.createFromString(testAccount.getAccessToken(), requestedPermissions,
- AccessTokenSource.TEST_USER);
- finishAuthOrReauth(accessToken, null);
- }
- private TestAccount createTestAccountAndFinishAuth() {
- Bundle parameters = new Bundle();
- parameters.putString("installed", "true");
- parameters.putString("permissions", getPermissionsString());
- parameters.putString("access_token", getAppAccessToken());
- // If we're in shared mode, we want to rename this user to encode its permissions, so we can find it later
- // in another shared session. If we're in private mode, don't bother renaming it since we're just going to
- // delete it at the end of the session.
- if (mode == Mode.SHARED) {
- parameters.putString("name", String.format("Shared %s Testuser", getSharedTestAccountIdentifier()));
- }
- String graphPath = String.format("%s/accounts/test-users", testApplicationId);
- Request createUserRequest = new Request(null, graphPath, parameters, HttpMethod.POST);
- Response response = createUserRequest.executeAndWait();
- FacebookRequestError error = response.getError();
- TestAccount testAccount = response.getGraphObjectAs(TestAccount.class);
- if (error != null) {
- finishAuthOrReauth(null, error.getException());
- return null;
- } else {
- assert testAccount != null;
- // If we are in shared mode, store this new account in the dictionary so we can re-use it later.
- if (mode == Mode.SHARED) {
- // Remember the new name we gave it, since we didn't get it back in the results of the create request.
- testAccount.setName(parameters.getString("name"));
- storeTestAccount(testAccount);
- }
- finishAuthWithTestAccount(testAccount);
- return testAccount;
- }
- }
- private void deleteTestAccount(String testAccountId, String appAccessToken) {
- Bundle parameters = new Bundle();
- parameters.putString("access_token", appAccessToken);
- Request request = new Request(null, testAccountId, parameters, HttpMethod.DELETE);
- Response response = request.executeAndWait();
- FacebookRequestError error = response.getError();
- GraphObject graphObject = response.getGraphObject();
- if (error != null) {
- Log.w(LOG_TAG, String.format("Could not delete test account %s: %s", testAccountId, error.getException().toString()));
- } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false) {
- Log.w(LOG_TAG, String.format("Could not delete test account %s: unknown reason", testAccountId));
- }
- }
- private String getPermissionsString() {
- return TextUtils.join(",", requestedPermissions);
- }
- private String getSharedTestAccountIdentifier() {
- // We use long even though hashes are ints to avoid sign issues.
- long permissionsHash = getPermissionsString().hashCode() & 0xffffffffL;
- long sessionTagHash = (sessionUniqueUserTag != null) ? sessionUniqueUserTag.hashCode() & 0xffffffffL : 0;
- long combinedHash = permissionsHash ^ sessionTagHash;
- return validNameStringFromInteger(combinedHash);
- }
- private String validNameStringFromInteger(long i) {
- String s = Long.toString(i);
- StringBuilder result = new StringBuilder("Perm");
- // We know each character is a digit. Convert it into a letter 'a'-'j'. Avoid repeated characters
- // that might make Facebook reject the name by converting every other repeated character into one
- // 10 higher ('k'-'t').
- char lastChar = 0;
- for (char c : s.toCharArray()) {
- if (c == lastChar) {
- c += 10;
- }
- result.append((char) (c + 'a' - '0'));
- lastChar = c;
- }
- return result.toString();
- }
- private interface TestAccount extends GraphObject {
- String getId();
- String getAccessToken();
- // Note: We don't actually get Name from our FQL query. We fill it in by correlating with UserAccounts.
- String getName();
- void setName(String name);
- }
- private interface UserAccount extends GraphObject {
- String getUid();
- String getName();
- void setName(String name);
- }
- private interface FqlResult extends GraphObject {
- GraphObjectList<GraphObject> getFqlResultSet();
- }
- private interface FqlResponse extends GraphObject {
- GraphObjectList<FqlResult> getData();
- }
- private static final class TestTokenCachingStrategy extends TokenCachingStrategy {
- private Bundle bundle;
- @Override
- public Bundle load() {
- return bundle;
- }
- @Override
- public void save(Bundle value) {
- bundle = value;
- }
- @Override
- public void clear() {
- bundle = null;
- }
- }
- }