PageRenderTime 104ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/facebookSDK/src/main/java/com/facebook/TestSession.java

https://gitlab.com/varunkothamachu/yelo-android
Java | 519 lines | 299 code | 90 blank | 130 comment | 51 complexity | 113425b310d9a9ac3e43d9a9a04c58aa MD5 | raw file
  1. /*
  2. *
  3. * * Copyright (C) 2015 yelo.red
  4. * *
  5. * * 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
  6. * *
  7. * * http://www.apache.org/licenses/LICENSE-2.0
  8. * *
  9. * * 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.
  10. *
  11. */
  12. package com.facebook;
  13. import android.app.Activity;
  14. import android.os.Bundle;
  15. import android.text.TextUtils;
  16. import android.util.Log;
  17. import com.facebook.internal.Logger;
  18. import com.facebook.internal.Utility;
  19. import com.facebook.internal.Validate;
  20. import com.facebook.model.GraphObject;
  21. import com.facebook.model.GraphObjectList;
  22. import org.json.JSONException;
  23. import org.json.JSONObject;
  24. import java.util.Arrays;
  25. import java.util.Collection;
  26. import java.util.Date;
  27. import java.util.HashMap;
  28. import java.util.List;
  29. import java.util.Map;
  30. /**
  31. * Implements an subclass of Session that knows about test users for a particular
  32. * application. This should never be used from a real application, but may be useful
  33. * for writing unit tests, etc.
  34. * <p/>
  35. * Facebook allows developers to create test accounts for testing their applications'
  36. * Facebook integration (see https://developers.facebook.com/docs/test_users/). This class
  37. * simplifies use of these accounts for writing unit tests. It is not designed for use in
  38. * production application code.
  39. * <p/>
  40. * The main use case for this class is using {@link #createSessionWithPrivateUser(android.app.Activity, java.util.List)}
  41. * or {@link #createSessionWithSharedUser(android.app.Activity, java.util.List)}
  42. * to create a session for a test user. Two modes are supported. In "shared" mode, an attempt
  43. * is made to find an existing test user that has the required permissions. If no such user is available,
  44. * a new one is created with the required permissions. In "private" mode, designed for
  45. * scenarios which require a new user in a known clean state, a new test user will always be
  46. * created, and it will be automatically deleted when the TestSession is closed. The session
  47. * obeys the same lifecycle as a regular Session, meaning it must be opened after creation before
  48. * it can be used to make calls to the Facebook API.
  49. * <p/>
  50. * Prior to creating a TestSession, two static methods must be called to initialize the
  51. * application ID and application Secret to be used for managing test users. These methods are
  52. * {@link #setTestApplicationId(String)} and {@link #setTestApplicationSecret(String)}.
  53. * <p/>
  54. * Note that the shared test user functionality depends on a naming convention for the test users.
  55. * It is important that any testing of functionality which will mutate the permissions for a
  56. * test user NOT use a shared test user, or this scheme will break down. If a shared test user
  57. * seems to be in an invalid state, it can be deleted manually via the Web interface at
  58. * https://developers.facebook.com/apps/APP_ID/permissions?role=test+users.
  59. */
  60. public class TestSession extends Session {
  61. private static final long serialVersionUID = 1L;
  62. private enum Mode {
  63. PRIVATE, SHARED
  64. }
  65. private static final String LOG_TAG = Logger.LOG_TAG_BASE + "TestSession";
  66. private static Map<String, TestAccount> appTestAccounts;
  67. private static String testApplicationSecret;
  68. private static String testApplicationId;
  69. private final String sessionUniqueUserTag;
  70. private final List<String> requestedPermissions;
  71. private final Mode mode;
  72. private String testAccountId;
  73. private boolean wasAskedToExtendAccessToken;
  74. TestSession(Activity activity, List<String> permissions, TokenCachingStrategy tokenCachingStrategy,
  75. String sessionUniqueUserTag, Mode mode) {
  76. super(activity, TestSession.testApplicationId, tokenCachingStrategy);
  77. Validate.notNull(permissions, "permissions");
  78. // Validate these as if they were arguments even though they are statics.
  79. Validate.notNullOrEmpty(testApplicationId, "testApplicationId");
  80. Validate.notNullOrEmpty(testApplicationSecret, "testApplicationSecret");
  81. this.sessionUniqueUserTag = sessionUniqueUserTag;
  82. this.mode = mode;
  83. this.requestedPermissions = permissions;
  84. }
  85. /**
  86. * Constructs a TestSession which creates a test user on open, and destroys the user on
  87. * close; This method should not be used in application code -- but is useful for creating unit tests
  88. * that use the Facebook SDK.
  89. *
  90. * @param activity the Activity to use for opening the session
  91. * @param permissions list of strings containing permissions to request; nil will result in
  92. * a common set of permissions (email, publish_actions) being requested
  93. * @return a new TestSession that is in the CREATED state, ready to be opened
  94. */
  95. public static TestSession createSessionWithPrivateUser(Activity activity, List<String> permissions) {
  96. return createTestSession(activity, permissions, Mode.PRIVATE, null);
  97. }
  98. /**
  99. * Constructs a TestSession which uses a shared test user with the right permissions,
  100. * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
  101. * tests).
  102. * <p/>
  103. * This method should not be used in application code -- but is useful for creating unit tests
  104. * that use the Facebook SDK.
  105. *
  106. * @param activity the Activity to use for opening the session
  107. * @param permissions list of strings containing permissions to request; nil will result in
  108. * a common set of permissions (email, publish_actions) being requested
  109. * @return a new TestSession that is in the CREATED state, ready to be opened
  110. */
  111. public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions) {
  112. return createSessionWithSharedUser(activity, permissions, null);
  113. }
  114. /**
  115. * Constructs a TestSession which uses a shared test user with the right permissions,
  116. * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
  117. * tests).
  118. * <p/>
  119. * This method should not be used in application code -- but is useful for creating unit tests
  120. * that use the Facebook SDK.
  121. *
  122. * @param activity the Activity to use for opening the session
  123. * @param permissions list of strings containing permissions to request; nil will result in
  124. * a common set of permissions (email, publish_actions) being requested
  125. * @param sessionUniqueUserTag a string which will be used to make this user unique among other
  126. * users with the same permissions. Useful for tests which require two or more users to interact
  127. * with each other, and which therefore must have sessions associated with different users.
  128. * @return a new TestSession that is in the CREATED state, ready to be opened
  129. */
  130. public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions,
  131. String sessionUniqueUserTag) {
  132. return createTestSession(activity, permissions, Mode.SHARED, sessionUniqueUserTag);
  133. }
  134. /**
  135. * Gets the Facebook Application ID for the application under test.
  136. *
  137. * @return the application ID
  138. */
  139. public static synchronized String getTestApplicationId() {
  140. return testApplicationId;
  141. }
  142. /**
  143. * Sets the Facebook Application ID for the application under test. This must be specified
  144. * prior to creating a TestSession.
  145. *
  146. * @param applicationId the application ID
  147. */
  148. public static synchronized void setTestApplicationId(String applicationId) {
  149. if (testApplicationId != null && !testApplicationId.equals(applicationId)) {
  150. throw new FacebookException("Can't have more than one test application ID");
  151. }
  152. testApplicationId = applicationId;
  153. }
  154. /**
  155. * Gets the Facebook Application Secret for the application under test.
  156. *
  157. * @return the application secret
  158. */
  159. public static synchronized String getTestApplicationSecret() {
  160. return testApplicationSecret;
  161. }
  162. /**
  163. * Sets the Facebook Application Secret for the application under test. This must be specified
  164. * prior to creating a TestSession.
  165. *
  166. * @param applicationSecret the application secret
  167. */
  168. public static synchronized void setTestApplicationSecret(String applicationSecret) {
  169. if (testApplicationSecret != null && !testApplicationSecret.equals(applicationSecret)) {
  170. throw new FacebookException("Can't have more than one test application secret");
  171. }
  172. testApplicationSecret = applicationSecret;
  173. }
  174. /**
  175. * Gets the ID of the test user that this TestSession is authenticated as.
  176. *
  177. * @return the Facebook user ID of the test user
  178. */
  179. public final String getTestUserId() {
  180. return testAccountId;
  181. }
  182. private static synchronized TestSession createTestSession(Activity activity, List<String> permissions, Mode mode,
  183. String sessionUniqueUserTag) {
  184. if (Utility.isNullOrEmpty(testApplicationId) || Utility.isNullOrEmpty(testApplicationSecret)) {
  185. throw new FacebookException("Must provide app ID and secret");
  186. }
  187. if (Utility.isNullOrEmpty(permissions)) {
  188. permissions = Arrays.asList("email", "publish_actions");
  189. }
  190. return new TestSession(activity, permissions, new TestTokenCachingStrategy(), sessionUniqueUserTag,
  191. mode);
  192. }
  193. private static synchronized void retrieveTestAccountsForAppIfNeeded() {
  194. if (appTestAccounts != null) {
  195. return;
  196. }
  197. appTestAccounts = new HashMap<String, TestAccount>();
  198. // The data we need is split across two different FQL tables. We construct two queries, submit them
  199. // together (the second one refers to the first one), then cross-reference the results.
  200. // Get the test accounts for this app.
  201. String testAccountQuery = String.format("SELECT id,access_token FROM test_account WHERE app_id = %s",
  202. testApplicationId);
  203. // Get the user names for those accounts.
  204. String userQuery = "SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)";
  205. Bundle parameters = new Bundle();
  206. // Build a JSON string that contains our queries and pass it as the 'q' parameter of the query.
  207. JSONObject multiquery;
  208. try {
  209. multiquery = new JSONObject();
  210. multiquery.put("test_accounts", testAccountQuery);
  211. multiquery.put("users", userQuery);
  212. } catch (JSONException exception) {
  213. throw new FacebookException(exception);
  214. }
  215. parameters.putString("q", multiquery.toString());
  216. // We need to authenticate as this app.
  217. parameters.putString("access_token", getAppAccessToken());
  218. Request request = new Request(null, "fql", parameters, null);
  219. Response response = request.executeAndWait();
  220. if (response.getError() != null) {
  221. throw response.getError().getException();
  222. }
  223. FqlResponse fqlResponse = response.getGraphObjectAs(FqlResponse.class);
  224. GraphObjectList<FqlResult> fqlResults = fqlResponse.getData();
  225. if (fqlResults == null || fqlResults.size() != 2) {
  226. throw new FacebookException("Unexpected number of results from FQL query");
  227. }
  228. // We get back two sets of results. The first is from the test_accounts query, the second from the users query.
  229. Collection<TestAccount> testAccounts = fqlResults.get(0).getFqlResultSet().castToListOf(TestAccount.class);
  230. Collection<UserAccount> userAccounts = fqlResults.get(1).getFqlResultSet().castToListOf(UserAccount.class);
  231. // Use both sets of results to populate our static array of accounts.
  232. populateTestAccounts(testAccounts, userAccounts);
  233. return;
  234. }
  235. private static synchronized void populateTestAccounts(Collection<TestAccount> testAccounts,
  236. Collection<UserAccount> userAccounts) {
  237. // We get different sets of data from each of these queries. We want to combine them into a single data
  238. // structure. We have added a Name property to the TestAccount interface, even though we don't really get
  239. // a name back from the service from that query. We stick the Name from the corresponding UserAccount in it.
  240. for (TestAccount testAccount : testAccounts) {
  241. storeTestAccount(testAccount);
  242. }
  243. for (UserAccount userAccount : userAccounts) {
  244. TestAccount testAccount = appTestAccounts.get(userAccount.getUid());
  245. if (testAccount != null) {
  246. testAccount.setName(userAccount.getName());
  247. }
  248. }
  249. }
  250. private static synchronized void storeTestAccount(TestAccount testAccount) {
  251. appTestAccounts.put(testAccount.getId(), testAccount);
  252. }
  253. private static synchronized TestAccount findTestAccountMatchingIdentifier(String identifier) {
  254. retrieveTestAccountsForAppIfNeeded();
  255. for (TestAccount testAccount : appTestAccounts.values()) {
  256. if (testAccount.getName().contains(identifier)) {
  257. return testAccount;
  258. }
  259. }
  260. return null;
  261. }
  262. @Override
  263. public final String toString() {
  264. String superString = super.toString();
  265. return new StringBuilder().append("{TestSession").append(" testUserId:").append(testAccountId)
  266. .append(" ").append(superString).append("}").toString();
  267. }
  268. @Override
  269. void authorize(AuthorizationRequest request) {
  270. if (mode == Mode.PRIVATE) {
  271. createTestAccountAndFinishAuth();
  272. } else {
  273. findOrCreateSharedTestAccount();
  274. }
  275. }
  276. @Override
  277. void postStateChange(final SessionState oldState, final SessionState newState, final Exception error) {
  278. // Make sure this doesn't get overwritten.
  279. String id = testAccountId;
  280. super.postStateChange(oldState, newState, error);
  281. if (newState.isClosed() && id != null && mode == Mode.PRIVATE) {
  282. deleteTestAccount(id, getAppAccessToken());
  283. }
  284. }
  285. boolean getWasAskedToExtendAccessToken() {
  286. return wasAskedToExtendAccessToken;
  287. }
  288. void forceExtendAccessToken(boolean forceExtendAccessToken) {
  289. AccessToken currentToken = getTokenInfo();
  290. setTokenInfo(
  291. new AccessToken(currentToken.getToken(), new Date(), currentToken.getPermissions(),
  292. AccessTokenSource.TEST_USER, new Date(0)));
  293. setLastAttemptedTokenExtendDate(new Date(0));
  294. }
  295. @Override
  296. boolean shouldExtendAccessToken() {
  297. boolean result = super.shouldExtendAccessToken();
  298. wasAskedToExtendAccessToken = false;
  299. return result;
  300. }
  301. @Override
  302. void extendAccessToken() {
  303. wasAskedToExtendAccessToken = true;
  304. super.extendAccessToken();
  305. }
  306. void fakeTokenRefreshAttempt() {
  307. setCurrentTokenRefreshRequest(new TokenRefreshRequest());
  308. }
  309. static final String getAppAccessToken() {
  310. return testApplicationId + "|" + testApplicationSecret;
  311. }
  312. private void findOrCreateSharedTestAccount() {
  313. TestAccount testAccount = findTestAccountMatchingIdentifier(getSharedTestAccountIdentifier());
  314. if (testAccount != null) {
  315. finishAuthWithTestAccount(testAccount);
  316. } else {
  317. createTestAccountAndFinishAuth();
  318. }
  319. }
  320. private void finishAuthWithTestAccount(TestAccount testAccount) {
  321. testAccountId = testAccount.getId();
  322. AccessToken accessToken = AccessToken.createFromString(testAccount.getAccessToken(), requestedPermissions,
  323. AccessTokenSource.TEST_USER);
  324. finishAuthOrReauth(accessToken, null);
  325. }
  326. private TestAccount createTestAccountAndFinishAuth() {
  327. Bundle parameters = new Bundle();
  328. parameters.putString("installed", "true");
  329. parameters.putString("permissions", getPermissionsString());
  330. parameters.putString("access_token", getAppAccessToken());
  331. // If we're in shared mode, we want to rename this user to encode its permissions, so we can find it later
  332. // in another shared session. If we're in private mode, don't bother renaming it since we're just going to
  333. // delete it at the end of the session.
  334. if (mode == Mode.SHARED) {
  335. parameters.putString("name", String.format("Shared %s Testuser", getSharedTestAccountIdentifier()));
  336. }
  337. String graphPath = String.format("%s/accounts/test-users", testApplicationId);
  338. Request createUserRequest = new Request(null, graphPath, parameters, HttpMethod.POST);
  339. Response response = createUserRequest.executeAndWait();
  340. FacebookRequestError error = response.getError();
  341. TestAccount testAccount = response.getGraphObjectAs(TestAccount.class);
  342. if (error != null) {
  343. finishAuthOrReauth(null, error.getException());
  344. return null;
  345. } else {
  346. assert testAccount != null;
  347. // If we are in shared mode, store this new account in the dictionary so we can re-use it later.
  348. if (mode == Mode.SHARED) {
  349. // Remember the new name we gave it, since we didn't get it back in the results of the create request.
  350. testAccount.setName(parameters.getString("name"));
  351. storeTestAccount(testAccount);
  352. }
  353. finishAuthWithTestAccount(testAccount);
  354. return testAccount;
  355. }
  356. }
  357. private void deleteTestAccount(String testAccountId, String appAccessToken) {
  358. Bundle parameters = new Bundle();
  359. parameters.putString("access_token", appAccessToken);
  360. Request request = new Request(null, testAccountId, parameters, HttpMethod.DELETE);
  361. Response response = request.executeAndWait();
  362. FacebookRequestError error = response.getError();
  363. GraphObject graphObject = response.getGraphObject();
  364. if (error != null) {
  365. Log.w(LOG_TAG, String.format("Could not delete test account %s: %s", testAccountId, error.getException().toString()));
  366. } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false) {
  367. Log.w(LOG_TAG, String.format("Could not delete test account %s: unknown reason", testAccountId));
  368. }
  369. }
  370. private String getPermissionsString() {
  371. return TextUtils.join(",", requestedPermissions);
  372. }
  373. private String getSharedTestAccountIdentifier() {
  374. // We use long even though hashes are ints to avoid sign issues.
  375. long permissionsHash = getPermissionsString().hashCode() & 0xffffffffL;
  376. long sessionTagHash = (sessionUniqueUserTag != null) ? sessionUniqueUserTag.hashCode() & 0xffffffffL : 0;
  377. long combinedHash = permissionsHash ^ sessionTagHash;
  378. return validNameStringFromInteger(combinedHash);
  379. }
  380. private String validNameStringFromInteger(long i) {
  381. String s = Long.toString(i);
  382. StringBuilder result = new StringBuilder("Perm");
  383. // We know each character is a digit. Convert it into a letter 'a'-'j'. Avoid repeated characters
  384. // that might make Facebook reject the name by converting every other repeated character into one
  385. // 10 higher ('k'-'t').
  386. char lastChar = 0;
  387. for (char c : s.toCharArray()) {
  388. if (c == lastChar) {
  389. c += 10;
  390. }
  391. result.append((char) (c + 'a' - '0'));
  392. lastChar = c;
  393. }
  394. return result.toString();
  395. }
  396. private interface TestAccount extends GraphObject {
  397. String getId();
  398. String getAccessToken();
  399. // Note: We don't actually get Name from our FQL query. We fill it in by correlating with UserAccounts.
  400. String getName();
  401. void setName(String name);
  402. }
  403. private interface UserAccount extends GraphObject {
  404. String getUid();
  405. String getName();
  406. void setName(String name);
  407. }
  408. private interface FqlResult extends GraphObject {
  409. GraphObjectList<GraphObject> getFqlResultSet();
  410. }
  411. private interface FqlResponse extends GraphObject {
  412. GraphObjectList<FqlResult> getData();
  413. }
  414. private static final class TestTokenCachingStrategy extends TokenCachingStrategy {
  415. private Bundle bundle;
  416. @Override
  417. public Bundle load() {
  418. return bundle;
  419. }
  420. @Override
  421. public void save(Bundle value) {
  422. bundle = value;
  423. }
  424. @Override
  425. public void clear() {
  426. bundle = null;
  427. }
  428. }
  429. }