/libraries/facebookSDK/src/main/java/com/facebook/TestSession.java
Java | 519 lines | 299 code | 90 blank | 130 comment | 51 complexity | 113425b310d9a9ac3e43d9a9a04c58aa MD5 | raw file
- /*
- *
- * * Copyright (C) 2015 yelo.red
- * *
- * * 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.internal.Logger;
- import com.facebook.internal.Utility;
- import com.facebook.internal.Validate;
- import com.facebook.model.GraphObject;
- import com.facebook.model.GraphObjectList;
- import org.json.JSONException;
- import org.json.JSONObject;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * 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;
- }
- }
- }