content: /**
   * 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 String testAccountUserName;
  
      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;
      }
  
      /**
       * Gets the name of the test user that this TestSession is authenticated as.
       *
       * @return the name of the test user
       */
      public final String getTestUserName() {
          return testAccountUserName;
      }
  
  
      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(),
                          currentToken.getDeclinedPermissions(), 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();
          testAccountUserName = testAccount.getName();
  
          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;
          }
      }
  }
  